diff --git a/.changeset/beige-lies-pay.md b/.changeset/beige-lies-pay.md new file mode 100644 index 0000000000..94206f9d9c --- /dev/null +++ b/.changeset/beige-lies-pay.md @@ -0,0 +1,12 @@ +--- +'@backstage/backend-common': patch +'@backstage/backend-tasks': patch +'@backstage/backend-test-utils': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-bazaar-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Do some groundwork for supporting the `better-sqlite3` driver, to maybe eventually replace `@vscode/sqlite3` (#9912) diff --git a/.changeset/big-meals-fly.md b/.changeset/big-meals-fly.md new file mode 100644 index 0000000000..be88400227 --- /dev/null +++ b/.changeset/big-meals-fly.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-scaffolder-backend-module-rails': patch +'@backstage/plugin-scaffolder-backend-module-yeoman': patch +--- + +Updating documentation for supporting `apiVersion: scaffolder.backstage.io/v1beta3` diff --git a/.changeset/big-months-deliver.md b/.changeset/big-months-deliver.md new file mode 100644 index 0000000000..1cf873beb1 --- /dev/null +++ b/.changeset/big-months-deliver.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added `--since ` flag for `repo build` command.` diff --git a/.changeset/brave-brooms-tie.md b/.changeset/brave-brooms-tie.md new file mode 100644 index 0000000000..a4b986c9d5 --- /dev/null +++ b/.changeset/brave-brooms-tie.md @@ -0,0 +1,33 @@ +--- +'@backstage/create-app': patch +--- + +Builtin support for cookiecutter based templates has been removed from `@backstage/plugin-scaffolder-backend`. Due to this, the `containerRunner` argument to its `createRouter` has also been removed. + +If you do not use cookiecutter templates and are fine with removing support from it in your own installation, update your `packages/backend/src/plugins/scaffolder.ts` file as follows: + +```diff +-import { DockerContainerRunner } from '@backstage/backend-common'; + import { CatalogClient } from '@backstage/catalog-client'; + import { createRouter } from '@backstage/plugin-scaffolder-backend'; +-import Docker from 'dockerode'; + import { Router } from 'express'; + import type { PluginEnvironment } from '../types'; + + export default async function createPlugin({ + reader, + discovery, + }: PluginEnvironment): Promise { +- const dockerClient = new Docker(); +- const containerRunner = new DockerContainerRunner({ dockerClient }); +- + const catalogClient = new CatalogClient({ discoveryApi: discovery }); +- + return await createRouter({ +- containerRunner, + logger, + config, + // ... +``` + +If you want to retain cookiecutter support, please use the `@backstage/plugin-scaffolder-backend-module-cookiecutter` package explicitly (see [its README](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend-module-cookiecutter) for installation instructions). diff --git a/.changeset/brown-points-impress.md b/.changeset/brown-points-impress.md new file mode 100644 index 0000000000..74d5e1019d --- /dev/null +++ b/.changeset/brown-points-impress.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +**BREAKING**: Removed the `AboutCard` component which has been replaced by `EntityAboutCard`. diff --git a/.changeset/chilly-comics-jam.md b/.changeset/chilly-comics-jam.md new file mode 100644 index 0000000000..92d7f9014e --- /dev/null +++ b/.changeset/chilly-comics-jam.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed `reduceCatalogFilters` and `reduceEntityFilters` due to low external utility value. diff --git a/.changeset/chilly-goats-suffer.md b/.changeset/chilly-goats-suffer.md deleted file mode 100644 index da98b4019b..0000000000 --- a/.changeset/chilly-goats-suffer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Applied fix from version 0.17.2 of this package, which is part of the v0.69.2 release of Backstage. diff --git a/.changeset/clever-garlics-rescue.md b/.changeset/clever-garlics-rescue.md new file mode 100644 index 0000000000..a7dd387042 --- /dev/null +++ b/.changeset/clever-garlics-rescue.md @@ -0,0 +1,20 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +**BREAKING**: Removed `GitLabDiscoveryProcessor`, which now instead should be imported from `@backstage/plugin-catalog-backend-module-gitlab`. NOTE THAT this processor was part of the default set of processors in the catalog backend, and if you are a user of discovery on GitLab, you MUST now add it manually in the catalog initialization code of your backend. + +```diff +// In packages/backend/src/plugins/catalog.ts ++import { GitLabDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-gitlab'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); ++ builder.addProcessor( ++ GitLabDiscoveryProcessor.fromConfig(env.config, { logger: env.logger }) ++ ); +``` + +**BREAKING**: Removed `AzureDevOpsDiscoveryProcessor`, which now instead should be imported from `@backstage/plugin-catalog-backend-module-azure`. This processor was not part of the set of default processors. If you were using it, you should already have a reference to it in your backend code and only need to update the import. diff --git a/.changeset/curvy-forks-cross.md b/.changeset/curvy-forks-cross.md new file mode 100644 index 0000000000..feced6ed0e --- /dev/null +++ b/.changeset/curvy-forks-cross.md @@ -0,0 +1,14 @@ +--- +'@backstage/create-app': patch +--- + +Removed the `cookiecutter-golang` template from the default `create-app` install as we no longer provide `cookiecutter` action out of the box. + +You can remove the template by removing the following lines from your `app-config.yaml` under `catalog.locations`: + +```diff +- - type: url +- target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml +- rules: +- - allow: [Template] +``` diff --git a/.changeset/dependabot-4014eb7.md b/.changeset/dependabot-4014eb7.md new file mode 100644 index 0000000000..7b4d64523a --- /dev/null +++ b/.changeset/dependabot-4014eb7.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +chore(deps): bump `minimatch` from 5.0.0 to 5.0.1 diff --git a/.changeset/dependabot-8a13aed.md b/.changeset/dependabot-8a13aed.md deleted file mode 100644 index 13075ccde2..0000000000 --- a/.changeset/dependabot-8a13aed.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/core-components': patch -'@backstage/plugin-search': patch -'@backstage/plugin-techdocs': patch ---- - -chore(deps): bump `react-text-truncate` from 0.17.0 to 0.18.0 diff --git a/.changeset/dependabot-ae5fb9c.md b/.changeset/dependabot-ae5fb9c.md new file mode 100644 index 0000000000..5648da2be7 --- /dev/null +++ b/.changeset/dependabot-ae5fb9c.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-gcp-projects': patch +--- + +chore(deps): bump `@react-hookz/web` from 12.3.0 to 13.0.0 diff --git a/.changeset/dependabot-d2397d3.md b/.changeset/dependabot-d2397d3.md new file mode 100644 index 0000000000..5f8bfbf59d --- /dev/null +++ b/.changeset/dependabot-d2397d3.md @@ -0,0 +1,19 @@ +--- +'@backstage/backend-common': patch +'@backstage/cli': patch +'@backstage/config-loader': patch +'@backstage/create-app': patch +'@techdocs/cli': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-rollbar-backend': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-scaffolder-backend-module-rails': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs-node': patch +--- + +build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 diff --git a/.changeset/eight-adults-joke.md b/.changeset/eight-adults-joke.md deleted file mode 100644 index ffda738213..0000000000 --- a/.changeset/eight-adults-joke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-analytics-module-ga': patch ---- - -Added CSP instructions to README diff --git a/.changeset/fair-ants-look.md b/.changeset/fair-ants-look.md new file mode 100644 index 0000000000..4e70a042f8 --- /dev/null +++ b/.changeset/fair-ants-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +**BREAKING**: Removed `AwsS3DiscoveryProcessor`, which now instead should be imported from `@backstage/plugin-catalog-backend-module-aws`. diff --git a/.changeset/fair-roses-hide.md b/.changeset/fair-roses-hide.md new file mode 100644 index 0000000000..8f2c7a6660 --- /dev/null +++ b/.changeset/fair-roses-hide.md @@ -0,0 +1,21 @@ +--- +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder-backend': minor +'@backstage/plugin-scaffolder-common': minor +--- + +The following deprecations are now breaking and have been removed: + +- **BREAKING**: Support for `backstage.io/v1beta2` Software Templates has been removed. Please migrate your legacy templates to the new `scaffolder.backstage.io/v1beta3` `apiVersion` by following the [migration guide](https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3) + +- **BREAKING**: Removed the deprecated `TemplateMetadata`. Please use `TemplateInfo` instead. + +- **BREAKING**: Removed the deprecated `context.baseUrl`. It's now available on `context.templateInfo.baseUrl`. + +- **BREAKING**: Removed the deprecated `DispatchResult`, use `TaskBrokerDispatchResult` instead. + +- **BREAKING**: Removed the deprecated `runCommand`, use `executeShellCommond` instead. + +- **BREAKING**: Removed the deprecated `Status` in favour of `TaskStatus` instead. + +- **BREAKING**: Removed the deprecated `TaskState` in favour of `CurrentClaimedTask` instead. diff --git a/.changeset/fair-turtles-fetch.md b/.changeset/fair-turtles-fetch.md new file mode 100644 index 0000000000..ca61489f05 --- /dev/null +++ b/.changeset/fair-turtles-fetch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +support Bitbucket Cloud's code search to discover catalog files (multiple per repo, Location entities for existing files only) diff --git a/.changeset/fast-paws-arrive.md b/.changeset/fast-paws-arrive.md deleted file mode 100644 index 9c5f4f444f..0000000000 --- a/.changeset/fast-paws-arrive.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Update the template to reflect the renaming of `DocsResultListItem` to `TechDocsSearchResultListItem` from `@backstage/plugin-techdocs`. - -To apply this change to an existing app, make the following change to `packages/app/src/components/search/SearchPage.tsx`: - -```diff --import { DocsResultListItem } from '@backstage/plugin-techdocs'; -+import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; -``` - -```diff - case 'techdocs': - return ( -- -``` - -The `TechDocsIndexPage` now uses `DefaultTechDocsHome` as fall back if no children is provided as `LegacyTechDocsHome` is marked as deprecated. If you do not use a custom techdocs homepage, you can therefore update your app to the following: - -```diff -- }> -- -- -+ } /> -``` diff --git a/.changeset/fifty-brooms-whisper.md b/.changeset/fifty-brooms-whisper.md new file mode 100644 index 0000000000..b4c45555a3 --- /dev/null +++ b/.changeset/fifty-brooms-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +**BREAKING**: Removed `CatalogResultListItemProps` and `CatalogResultListItem`, replaced by `CatalogSearchResultListItemProps` and `CatalogSearchResultListItem`. diff --git a/.changeset/fuzzy-roses-swim.md b/.changeset/fuzzy-roses-swim.md new file mode 100644 index 0000000000..234d79c96a --- /dev/null +++ b/.changeset/fuzzy-roses-swim.md @@ -0,0 +1,7 @@ +--- +'@backstage/catalog-model': patch +--- + +Updated `parseEntityRef` to allow `:` and `/` in the entity name. For example, parsing `'component:default/foo:bar'` will result in the name `'foo:bar'`. + +Note that only parsing `'foo:bar'` itself will result in the name `'bar'` and the entity kind `'foo'`, meaning this is a particularly nasty trap for user defined entity references. For this reason it is strongly discouraged to use names that contain these characters, and the catalog model does not allow it by default. However, this change now makes is possible to use these names if the default catalog validation is replaced, and in particular a high level of automation of the catalog population can limit issues that it might otherwise cause. diff --git a/.changeset/gentle-dancers-greet.md b/.changeset/gentle-dancers-greet.md new file mode 100644 index 0000000000..bf160a7099 --- /dev/null +++ b/.changeset/gentle-dancers-greet.md @@ -0,0 +1,7 @@ +--- +'@backstage/cli': patch +--- + +Added a new ESLint configuration setup for packages, which utilizes package roles to generate the correct configuration. The new configuration is available at `@backstage/cli/config/eslint-factory`. + +Introduced a new `backstage-cli migrate package-lint-configs` command, which migrates old lint configurations to use `@backstage/cli/config/eslint-factory`. diff --git a/.changeset/giant-bottles-eat.md b/.changeset/giant-bottles-eat.md new file mode 100644 index 0000000000..d37343bfe4 --- /dev/null +++ b/.changeset/giant-bottles-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-airbrake': patch +--- + +Fix a bug where API calls were being made and errors were being added to the snack bar when no project ID was present. This is a common use case for components that haven't added the Airbrake plugin annotation to their `catalog-info.yaml`. diff --git a/.changeset/gorgeous-trains-clap.md b/.changeset/gorgeous-trains-clap.md new file mode 100644 index 0000000000..d0abf6148a --- /dev/null +++ b/.changeset/gorgeous-trains-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Added an `allowedKinds` option to `CatalogKindHeader` to limit entity kinds available in the dropdown. diff --git a/.changeset/great-hounds-allow.md b/.changeset/great-hounds-allow.md new file mode 100644 index 0000000000..54405fcf94 --- /dev/null +++ b/.changeset/great-hounds-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Updated `CatalogKindHeader` to respond to external changes to query parameters in the URL, such as two sidebar links that apply different catalog filters. diff --git a/.changeset/khaki-socks-wash.md b/.changeset/khaki-socks-wash.md deleted file mode 100644 index 241ac75be3..0000000000 --- a/.changeset/khaki-socks-wash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Set timeout for scaffolder octokit client diff --git a/.changeset/kind-experts-lay.md b/.changeset/kind-experts-lay.md new file mode 100644 index 0000000000..00524bfe3a --- /dev/null +++ b/.changeset/kind-experts-lay.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed the deprecated `loadCatalogOwnerRefs` function. Usages of this function can be directly replaced with `ownershipEntityRefs` from `identityApi.getBackstageIdentity()`. + +This also affects the `useEntityOwnership` hook in that it no longer uses `loadCatalogOwnerRefs`, meaning it will no longer load in additional relations and instead only rely on the `ownershipEntityRefs` from the `IdentityApi`. diff --git a/.changeset/late-pianos-attend.md b/.changeset/late-pianos-attend.md new file mode 100644 index 0000000000..e1e0aac392 --- /dev/null +++ b/.changeset/late-pianos-attend.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed `useEntityFromUrl`. diff --git a/.changeset/lazy-points-explain.md b/.changeset/lazy-points-explain.md new file mode 100644 index 0000000000..ced31c2195 --- /dev/null +++ b/.changeset/lazy-points-explain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +**BREAKING**: Removed `ScaffolderTaskOutput.entityRef` and `ScaffolderTaskOutput.remoteUrl`, which both have been deprecated for over a year. Please use the `links` output instead. diff --git a/.changeset/little-carpets-itch.md b/.changeset/little-carpets-itch.md deleted file mode 100644 index 6a4d95c140..0000000000 --- a/.changeset/little-carpets-itch.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Added some deprecations as follows: - -- **DEPRECATED**: `TemplateCardComponent` and `TaskPageComponent` props have been deprecated, and moved to a `components` prop instead. You can pass them in through there instead. -- **DEPRECATED**: `TemplateList` and `TemplateListProps` has been deprecated. Please use the `TemplateCard` to create your own list component instead. -- **DEPRECATED**: `setSecret` has been deprecated in favour of `setSecrets` when calling `useTemplateSecrets` - -Other notable changes: - -- `scaffolderApi.scaffold()` `values` type has been narrowed from `Record` to `Record` instead. -- Moved all navigation internally over to using `routeRefs` and `subRouteRefs` diff --git a/.changeset/lovely-goats-press.md b/.changeset/lovely-goats-press.md deleted file mode 100644 index fc3c0b4763..0000000000 --- a/.changeset/lovely-goats-press.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Collapse techdocs sidebar on small devices diff --git a/.changeset/many-coins-drive.md b/.changeset/many-coins-drive.md new file mode 100644 index 0000000000..b28a0b0519 --- /dev/null +++ b/.changeset/many-coins-drive.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +**BREAKING**: + +- Removed the `createFetchCookiecutterAction` export, please use the `@backstage/plugin-scaffolder-backend-module-cookiecutter` package explicitly (see [its README](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend-module-cookiecutter) for installation instructions). +- Removed the `containerRunner` argument from the types `RouterOptions` (as used by `createRouter`) and `CreateBuiltInActionsOptions` (as used by `createBuiltinActions`). diff --git a/.changeset/many-swans-sort.md b/.changeset/many-swans-sort.md new file mode 100644 index 0000000000..fec613cf00 --- /dev/null +++ b/.changeset/many-swans-sort.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +**BREAKING**: + +- Removed the previously deprecated `runPeriodically` export. Please use the `@backstage/backend-tasks` package instead, or copy [the actual implementation](https://github.com/backstage/backstage/blob/02875d4d56708c60f86f6b0a5b3da82e24988354/plugins/catalog-backend/src/util/runPeriodically.ts#L29) into your own code if you explicitly do not want coordination of task runs across your worker nodes. +- Removed the previously deprecated `CatalogProcessorLocationResult.optional` field. Please set the corresponding `LocationSpec.presence` field to `'optional'` instead. +- Related to the previous point, the `processingResult.location` function no longer has a second boolean `optional` argument. Please set the corresponding `LocationSpec.presence` field to `'optional'` instead. +- Removed the previously deprecated `StaticLocationProcessor`. It has not been in use for some time; its functionality is covered by `ConfigLocationEntityProvider` instead. diff --git a/.changeset/many-tools-buy.md b/.changeset/many-tools-buy.md new file mode 100644 index 0000000000..c2a617edfd --- /dev/null +++ b/.changeset/many-tools-buy.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-tasks': patch +--- + +Relaxed the task ID requirement to now support any non-empty string diff --git a/.changeset/mean-panthers-wonder.md b/.changeset/mean-panthers-wonder.md new file mode 100644 index 0000000000..1659c855f8 --- /dev/null +++ b/.changeset/mean-panthers-wonder.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Render markdown for description in software templates diff --git a/.changeset/metal-glasses-join.md b/.changeset/metal-glasses-join.md new file mode 100644 index 0000000000..aa7c6ecb9e --- /dev/null +++ b/.changeset/metal-glasses-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +add support for ETag at `BitbucketUrlReader.readUrl` diff --git a/.changeset/modern-windows-brush.md b/.changeset/modern-windows-brush.md new file mode 100644 index 0000000000..49c45eb41e --- /dev/null +++ b/.changeset/modern-windows-brush.md @@ -0,0 +1,9 @@ +--- +'@backstage/catalog-model': minor +--- + +**BREAKING**: + +- Removed the previously deprecated type `EntityRef`. Please use `string` for stringified entity refs, `CompoundEntityRef` for compound kind-namespace-name triplet objects, or custom objects like `{ kind?: string; namespace?: string; name: string }` and similar if you have need for partial types. +- Removed the previously deprecated type `LocationSpec` type, which has been moved to `@backstage/plugin-catalog-backend`. +- Removed the previously deprecated function `parseEntityName`. Please use `parseEntityRef` instead. diff --git a/.changeset/nasty-seahorses-bathe.md b/.changeset/nasty-seahorses-bathe.md new file mode 100644 index 0000000000..884313e422 --- /dev/null +++ b/.changeset/nasty-seahorses-bathe.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-rollbar': patch +--- + +Removed usage of removed hook. diff --git a/.changeset/neat-tools-design.md b/.changeset/neat-tools-design.md new file mode 100644 index 0000000000..92a52f0751 --- /dev/null +++ b/.changeset/neat-tools-design.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Add `setupRequestMockHandlers` which sets up a good `msw` server foundation, copied from `@backstage/test-utils` which is a frontend-only package and should not be used from backends. diff --git a/.changeset/new-books-protect.md b/.changeset/new-books-protect.md new file mode 100644 index 0000000000..568710ccd0 --- /dev/null +++ b/.changeset/new-books-protect.md @@ -0,0 +1,30 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': minor +--- + +**BREAKING**: Added a `schedule` field to `LdapOrgEntityProvider.fromConfig`, which is required. If you want to retain the old behavior of scheduling the provider manually, you can set it to the string value `'manual'`. But you may want to leverage the ability to instead pass in the recurring task schedule information directly. This will allow you to simplify your backend setup code to not need an intermediate variable and separate scheduling code at the bottom. + +All things said, a typical setup might now look as follows: + +```diff + // packages/backend/src/plugins/catalog.ts ++import { Duration } from 'luxon'; ++import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap'; + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); ++ // The target parameter below needs to match the ldap.providers.target ++ // value specified in your app-config. ++ builder.addEntityProvider( ++ LdapOrgEntityProvider.fromConfig(env.config, { ++ id: 'our-ldap-master', ++ target: 'ldaps://ds.example.net', ++ logger: env.logger, ++ schedule: env.scheduler.createScheduledTaskRunner({ ++ frequency: Duration.fromObject({ minutes: 60 }), ++ timeout: Duration.fromObject({ minutes: 15 }), ++ }), ++ }), ++ ); +``` diff --git a/.changeset/nice-windows-push.md b/.changeset/nice-windows-push.md new file mode 100644 index 0000000000..2d04141cf1 --- /dev/null +++ b/.changeset/nice-windows-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed `useEntityCompoundName`, use `useRouteRefParams(entityRouteRef)` instead. diff --git a/.changeset/nine-frogs-yell.md b/.changeset/nine-frogs-yell.md new file mode 100644 index 0000000000..7513bea4a6 --- /dev/null +++ b/.changeset/nine-frogs-yell.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Removed the following previously deprecated exports: + +- **BREAKING**: Removed the deprecated `TemplateList` component and the `TemplateListProps` type. Please use the `TemplateCard` to create your own list component instead to render these lists. + +- **BREAKING**: Removed the deprecated `setSecret` method, please use `setSecrets` instead. + +- **BREAKING**: Removed the deprecated `TemplateCardComponent` and `TaskPageComponent` props from the `ScaffolderPage` component. These are now provided using the `components` prop with the shape `{{ TemplateCardComponent: () => JSX.Element, TaskPageComponent: () => JSX.Element }}` + +- **BREAKING**: Removed `JobStatus` as this type was actually a legacy type used in `v1alpha` templates and the workflow engine and should no longer be used or depended on. diff --git a/.changeset/olive-emus-speak.md b/.changeset/olive-emus-speak.md new file mode 100644 index 0000000000..3cb6a775cf --- /dev/null +++ b/.changeset/olive-emus-speak.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-badges-backend': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-fossa': patch +--- + +Remove usages of now-removed `CatalogApi.getEntityByName` diff --git a/.changeset/olive-roses-sleep.md b/.changeset/olive-roses-sleep.md new file mode 100644 index 0000000000..b402a5b2aa --- /dev/null +++ b/.changeset/olive-roses-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Updated the template to write the Backstage release version to `backstage.json`, rather than the version of `@backstage/create-app`. This change is applied automatically when running `backstage-cli versions:bump` in the latest version of the Backstage CLI. diff --git a/.changeset/patched.json b/.changeset/patched.json deleted file mode 100644 index 486cfe13a5..0000000000 --- a/.changeset/patched.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "currentReleaseVersion": { - "@backstage/plugin-scaffolder-backend": "0.17.2" - } -} diff --git a/.changeset/pink-poets-grin.md b/.changeset/pink-poets-grin.md new file mode 100644 index 0000000000..d88431e9d5 --- /dev/null +++ b/.changeset/pink-poets-grin.md @@ -0,0 +1,11 @@ +--- +'@backstage/backend-common': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-rollbar-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Use `setupRequestMockHandlers` from `@backstage/backend-test-utils` diff --git a/.changeset/polite-apples-relax.md b/.changeset/polite-apples-relax.md new file mode 100644 index 0000000000..f6d10aaabf --- /dev/null +++ b/.changeset/polite-apples-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed display of the location in the log message that is printed when entity envelope validation fails. diff --git a/.changeset/popular-boxes-develop.md b/.changeset/popular-boxes-develop.md new file mode 100644 index 0000000000..65aae2c7df --- /dev/null +++ b/.changeset/popular-boxes-develop.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Applied the fix from version `0.15.1` of this package, which was part of the `v0.70.1` release of Backstage. diff --git a/.changeset/popular-rats-return.md b/.changeset/popular-rats-return.md new file mode 100644 index 0000000000..3a91e6d707 --- /dev/null +++ b/.changeset/popular-rats-return.md @@ -0,0 +1,9 @@ +--- +'@backstage/backend-common': patch +'@backstage/catalog-model': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-common': patch +'@backstage/plugin-catalog-react': patch +--- + +Applied the fix for the `/alpha` entry point resolution that was part of the `v0.70.1` release of Backstage. diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..8791f584d6 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,215 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "example-app": "0.2.67", + "@backstage/app-defaults": "0.2.0", + "example-backend": "0.2.67", + "@backstage/backend-common": "0.12.0", + "@backstage/backend-tasks": "0.1.10", + "@backstage/backend-test-utils": "0.1.20", + "@backstage/catalog-client": "0.8.0", + "@backstage/catalog-model": "0.12.0", + "@backstage/cli": "0.15.0", + "@backstage/cli-common": "0.1.8", + "@backstage/codemods": "0.1.34", + "@backstage/config": "0.1.15", + "@backstage/config-loader": "0.9.6", + "@backstage/core-app-api": "0.6.0", + "@backstage/core-components": "0.9.0", + "@backstage/core-plugin-api": "0.8.0", + "@backstage/create-app": "0.4.22", + "@backstage/dev-utils": "0.2.24", + "e2e-test": "0.2.0", + "@backstage/errors": "0.2.2", + "@backstage/integration": "0.8.0", + "@backstage/integration-react": "0.1.24", + "@backstage/release-manifests": "0.0.2", + "@backstage/search-common": "0.3.0", + "@techdocs/cli": "0.8.16", + "techdocs-cli-embedded-app": "0.2.66", + "@backstage/techdocs-common": "0.11.11", + "@backstage/test-utils": "0.3.0", + "@backstage/theme": "0.2.15", + "@backstage/types": "0.1.3", + "@backstage/version-bridge": "0.1.2", + "@backstage/plugin-airbrake": "0.3.1", + "@backstage/plugin-airbrake-backend": "0.2.1", + "@backstage/plugin-allure": "0.1.17", + "@backstage/plugin-analytics-module-ga": "0.1.12", + "@backstage/plugin-apache-airflow": "0.1.9", + "@backstage/plugin-api-docs": "0.8.1", + "@backstage/plugin-app-backend": "0.3.28", + "@backstage/plugin-auth-backend": "0.12.0", + "@backstage/plugin-auth-node": "0.1.4", + "@backstage/plugin-azure-devops": "0.1.17", + "@backstage/plugin-azure-devops-backend": "0.3.7", + "@backstage/plugin-azure-devops-common": "0.2.2", + "@backstage/plugin-badges": "0.2.25", + "@backstage/plugin-badges-backend": "0.1.22", + "@backstage/plugin-bazaar": "0.1.16", + "@backstage/plugin-bazaar-backend": "0.1.12", + "@backstage/plugin-bitrise": "0.1.28", + "@backstage/plugin-catalog": "0.9.1", + "@backstage/plugin-catalog-backend": "0.23.0", + "@backstage/plugin-catalog-backend-module-aws": "0.1.1", + "@backstage/plugin-catalog-backend-module-azure": "0.0.0", + "@backstage/plugin-catalog-backend-module-gitlab": "0.0.0", + "@backstage/plugin-catalog-backend-module-ldap": "0.3.15", + "@backstage/plugin-catalog-backend-module-msgraph": "0.2.18", + "@backstage/plugin-catalog-common": "0.2.0", + "@backstage/plugin-catalog-graph": "0.2.13", + "@backstage/plugin-catalog-graphql": "0.3.5", + "@backstage/plugin-catalog-import": "0.8.4", + "@backstage/plugin-catalog-react": "0.8.0", + "@backstage/plugin-cicd-statistics": "0.1.3", + "@backstage/plugin-circleci": "0.3.1", + "@backstage/plugin-cloudbuild": "0.3.1", + "@backstage/plugin-code-climate": "0.1.1", + "@backstage/plugin-code-coverage": "0.1.28", + "@backstage/plugin-code-coverage-backend": "0.1.26", + "@backstage/plugin-config-schema": "0.1.24", + "@backstage/plugin-cost-insights": "0.11.23", + "@backstage/plugin-explore": "0.3.32", + "@backstage/plugin-explore-react": "0.0.14", + "@backstage/plugin-firehydrant": "0.1.18", + "@backstage/plugin-fossa": "0.2.33", + "@backstage/plugin-gcp-projects": "0.3.20", + "@backstage/plugin-git-release-manager": "0.3.14", + "@backstage/plugin-github-actions": "0.5.1", + "@backstage/plugin-github-deployments": "0.1.32", + "@backstage/plugin-gitops-profiles": "0.3.19", + "@backstage/plugin-gocd": "0.1.7", + "@backstage/plugin-graphiql": "0.2.33", + "@backstage/plugin-graphql-backend": "0.1.18", + "@backstage/plugin-home": "0.4.17", + "@backstage/plugin-ilert": "0.1.27", + "@backstage/plugin-jenkins": "0.7.0", + "@backstage/plugin-jenkins-backend": "0.1.17", + "@backstage/plugin-jenkins-common": "0.1.0", + "@backstage/plugin-kafka": "0.3.1", + "@backstage/plugin-kafka-backend": "0.2.21", + "@backstage/plugin-kubernetes": "0.6.1", + "@backstage/plugin-kubernetes-backend": "0.4.11", + "@backstage/plugin-kubernetes-common": "0.2.6", + "@backstage/plugin-lighthouse": "0.3.1", + "@backstage/plugin-newrelic": "0.3.19", + "@backstage/plugin-newrelic-dashboard": "0.1.9", + "@backstage/plugin-org": "0.5.1", + "@backstage/plugin-pagerduty": "0.3.28", + "@backstage/plugin-periskop": "0.0.0", + "@backstage/plugin-periskop-backend": "0.0.0", + "@backstage/plugin-permission-backend": "0.5.3", + "@backstage/plugin-permission-common": "0.5.2", + "@backstage/plugin-permission-node": "0.5.3", + "@backstage/plugin-permission-react": "0.3.3", + "@backstage/plugin-proxy-backend": "0.2.22", + "@backstage/plugin-rollbar": "0.4.1", + "@backstage/plugin-rollbar-backend": "0.1.25", + "@backstage/plugin-scaffolder": "0.14.0", + "@backstage/plugin-scaffolder-backend": "0.17.3", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.3", + "@backstage/plugin-scaffolder-backend-module-rails": "0.3.3", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.1", + "@backstage/plugin-scaffolder-common": "0.2.3", + "@backstage/plugin-search": "0.7.2", + "@backstage/plugin-search-backend": "0.4.6", + "@backstage/plugin-search-backend-module-elasticsearch": "0.1.0", + "@backstage/plugin-search-backend-module-pg": "0.3.0", + "@backstage/plugin-search-backend-node": "0.5.0", + "@backstage/plugin-search-common": "0.3.0", + "@backstage/plugin-sentry": "0.3.39", + "@backstage/plugin-shortcuts": "0.2.2", + "@backstage/plugin-sonarqube": "0.3.1", + "@backstage/plugin-splunk-on-call": "0.3.25", + "@backstage/plugin-tech-insights": "0.1.11", + "@backstage/plugin-tech-insights-backend": "0.2.8", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.12", + "@backstage/plugin-tech-insights-common": "0.2.3", + "@backstage/plugin-tech-insights-node": "0.2.6", + "@backstage/plugin-tech-radar": "0.5.8", + "@backstage/plugin-techdocs": "0.15.0", + "@backstage/plugin-techdocs-backend": "0.14.1", + "@backstage/plugin-techdocs-node": "0.11.11", + "@backstage/plugin-todo": "0.2.3", + "@backstage/plugin-todo-backend": "0.1.25", + "@backstage/plugin-user-settings": "0.4.0", + "@backstage/plugin-xcmetrics": "0.2.21" + }, + "changesets": [ + "beige-lies-pay", + "big-meals-fly", + "big-months-deliver", + "brave-brooms-tie", + "brown-points-impress", + "chilly-comics-jam", + "clever-garlics-rescue", + "curvy-forks-cross", + "dependabot-4014eb7", + "dependabot-ae5fb9c", + "dependabot-d2397d3", + "fair-ants-look", + "fair-roses-hide", + "fair-turtles-fetch", + "fifty-brooms-whisper", + "fuzzy-roses-swim", + "gentle-dancers-greet", + "great-hounds-allow", + "kind-experts-lay", + "late-pianos-attend", + "lazy-points-explain", + "many-coins-drive", + "many-swans-sort", + "many-tools-buy", + "mean-panthers-wonder", + "metal-glasses-join", + "modern-windows-brush", + "nasty-seahorses-bathe", + "neat-tools-design", + "new-books-protect", + "nice-windows-push", + "nine-frogs-yell", + "olive-emus-speak", + "olive-roses-sleep", + "pink-poets-grin", + "polite-apples-relax", + "popular-boxes-develop", + "popular-rats-return", + "pretty-vans-unite", + "proud-readers-nail", + "quiet-pens-wait", + "quiet-seals-fix", + "red-kiwis-divide", + "rich-ravens-clap", + "search-byta-namnet", + "search-common-people", + "search-talking-in-code", + "search-wellington-paranormal", + "shaggy-apricots-hug", + "shiny-eels-mix", + "silent-cats-kneel", + "slow-vans-rhyme", + "something-else-here", + "spotty-seals-press", + "spotty-swans-run", + "strong-beds-attack", + "strong-pandas-roll", + "swift-mails-sing", + "tasty-carpets-fold", + "techdocs-byta-namnet", + "techdocs-empty-office", + "techdocs-node-one", + "ten-queens-dance", + "ten-rats-join", + "tender-berries-lie", + "thick-games-dress", + "thick-gifts-cheat", + "twenty-birds-think", + "twenty-fireants-turn", + "twenty-planes-dress", + "two-mails-boil", + "warm-bananas-behave", + "weak-schools-wash", + "young-feet-flow" + ] +} diff --git a/.changeset/pretty-vans-unite.md b/.changeset/pretty-vans-unite.md new file mode 100644 index 0000000000..318e86d069 --- /dev/null +++ b/.changeset/pretty-vans-unite.md @@ -0,0 +1,11 @@ +--- +'@backstage/cli': patch +--- + +Package roles are now marked as stable and migration is encouraged. Please check out the [migration guide](https://backstage.io/docs/tutorials/package-role-migration). + +The new `package`, `repo`, and `migrate` command categories are now marked as stable. + +Marked all commands that are being replaced by the new `package` and `repo` commands as deprecated. + +The package templates used by the `create` command have all been updated to use package roles. diff --git a/.changeset/proud-readers-nail.md b/.changeset/proud-readers-nail.md new file mode 100644 index 0000000000..9b5a6b980c --- /dev/null +++ b/.changeset/proud-readers-nail.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-airbrake-backend': patch +'@backstage/plugin-code-climate': patch +--- + +Added `backstage.role` to `package.json` diff --git a/.changeset/quiet-pens-wait.md b/.changeset/quiet-pens-wait.md new file mode 100644 index 0000000000..b7510eab3f --- /dev/null +++ b/.changeset/quiet-pens-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed `useEntityListProvider` use `useEntityList` instead. diff --git a/.changeset/quiet-seals-fix.md b/.changeset/quiet-seals-fix.md new file mode 100644 index 0000000000..d7c98958be --- /dev/null +++ b/.changeset/quiet-seals-fix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': patch +--- + +Ignore search referrals instead of throwing an error. diff --git a/.changeset/red-kiwis-divide.md b/.changeset/red-kiwis-divide.md new file mode 100644 index 0000000000..0bfb247093 --- /dev/null +++ b/.changeset/red-kiwis-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +**BREAKING**: Removed the previously deprecated `OctokitProvider` class. diff --git a/.changeset/rich-ravens-clap.md b/.changeset/rich-ravens-clap.md new file mode 100644 index 0000000000..913f87085f --- /dev/null +++ b/.changeset/rich-ravens-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': minor +--- + +Added package, moving out azure specific functionality from the catalog-backend diff --git a/.changeset/search-byta-namnet.md b/.changeset/search-byta-namnet.md new file mode 100644 index 0000000000..c3abc81996 --- /dev/null +++ b/.changeset/search-byta-namnet.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-common': patch +--- + +Renamed `@backstage/search-common` to `@backstage/plugin-search-common`. diff --git a/.changeset/search-common-people.md b/.changeset/search-common-people.md new file mode 100644 index 0000000000..2ed34c7573 --- /dev/null +++ b/.changeset/search-common-people.md @@ -0,0 +1,7 @@ +--- +'@backstage/search-common': patch +--- + +**DEPRECATION** + +The `@backstage/search-common` package is being renamed `@backstage/plugin-search-common`. We may continue to publish changes to `@backstage/search-common` for a time, but will stop doing so in the near future. If you depend on this package, you should update your dependencies to point at the renamed package. diff --git a/.changeset/search-talking-in-code.md b/.changeset/search-talking-in-code.md new file mode 100644 index 0000000000..46de5f8343 --- /dev/null +++ b/.changeset/search-talking-in-code.md @@ -0,0 +1,7 @@ +--- +'@backstage/create-app': patch +--- + +Postgres-based search is now installed when PG is chosen as the desired database for Backstage. + +There is no need to make this change in an existing Backstage backend. See [supported search engines](https://backstage.io/docs/features/search/search-engines) for details about production-ready search engines. diff --git a/.changeset/search-wellington-paranormal.md b/.changeset/search-wellington-paranormal.md new file mode 100644 index 0000000000..7cd1ead2e8 --- /dev/null +++ b/.changeset/search-wellington-paranormal.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-search-backend-module-elasticsearch': patch +'@backstage/plugin-search-backend-module-pg': patch +'@backstage/plugin-search-backend-node': patch +'@backstage/plugin-search-backend': patch +'@backstage/plugin-search': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs-node': patch +--- + +Use `@backstage/plugin-search-common` package instead of `@backstage/search-common`. diff --git a/.changeset/shaggy-apricots-hug.md b/.changeset/shaggy-apricots-hug.md new file mode 100644 index 0000000000..62bea4c3bd --- /dev/null +++ b/.changeset/shaggy-apricots-hug.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': patch +--- + +Tech Radar Ring names are now coloured from theme via theme.palette.text.primary (instead of a hard coded colour) diff --git a/.changeset/shaggy-readers-explain.md b/.changeset/shaggy-readers-explain.md deleted file mode 100644 index 29266e5a32..0000000000 --- a/.changeset/shaggy-readers-explain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Cleanup API report diff --git a/.changeset/shiny-eels-mix.md b/.changeset/shiny-eels-mix.md new file mode 100644 index 0000000000..2d886e9a7f --- /dev/null +++ b/.changeset/shiny-eels-mix.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Add names to sidebar sub menu styles for customization diff --git a/.changeset/silent-cats-kneel.md b/.changeset/silent-cats-kneel.md new file mode 100644 index 0000000000..9bd9cce0e0 --- /dev/null +++ b/.changeset/silent-cats-kneel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': minor +--- + +Added package, moving out GitLab specific functionality from the catalog-backend diff --git a/.changeset/silver-chairs-compete.md b/.changeset/silver-chairs-compete.md deleted file mode 100644 index 356cd2043b..0000000000 --- a/.changeset/silver-chairs-compete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-plugin-api': minor ---- - -**BREAKING**: OAuth provider id is now required when passing a provider to `createAuthRequester`. diff --git a/.changeset/slow-vans-rhyme.md b/.changeset/slow-vans-rhyme.md new file mode 100644 index 0000000000..6de924899f --- /dev/null +++ b/.changeset/slow-vans-rhyme.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': minor +--- + +**BREAKING**: The default validator for `metadata.tags` now permits the colon (`:`) character as well. diff --git a/.changeset/smart-items-fry.md b/.changeset/smart-items-fry.md deleted file mode 100644 index 625db82ce7..0000000000 --- a/.changeset/smart-items-fry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Applied fix from `v0.17.1` of this package which is part of the `v0.69.1` release of Backstage. diff --git a/.changeset/something-else-here.md b/.changeset/something-else-here.md new file mode 100644 index 0000000000..46e5efcb07 --- /dev/null +++ b/.changeset/something-else-here.md @@ -0,0 +1,6 @@ +--- +'@backstage/create-app': patch +'@backstage/plugin-catalog-backend': minor +--- + +- **BREAKING**: Support for `backstage.io/v1beta2` Software Templates has been removed. Please migrate your legacy templates to the new `scaffolder.backstage.io/v1beta3` `apiVersion` by following the [migration guide](https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3) diff --git a/.changeset/spicy-doors-relax.md b/.changeset/spicy-doors-relax.md deleted file mode 100644 index 3007f50d0e..0000000000 --- a/.changeset/spicy-doors-relax.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Deprecated `getEntityMetadataEditUrl` and `getEntityMetadataViewUrl` as these just return one annotation from the entity passed in. diff --git a/.changeset/spicy-lies-grin.md b/.changeset/spicy-lies-grin.md deleted file mode 100644 index b7e020f1db..0000000000 --- a/.changeset/spicy-lies-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-ldap': patch ---- - -Fixed bug in Catalog LDAP module to acknowledge page events to continue receiving entries if pagePause=true diff --git a/.changeset/spotty-seals-press.md b/.changeset/spotty-seals-press.md new file mode 100644 index 0000000000..a0a69b2efc --- /dev/null +++ b/.changeset/spotty-seals-press.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed `useOwnedEntities` and moved its usage internally to the scaffolder-backend where it's used. + +**BREAKING**: Removed `EntityTypeReturn` type which is now inlined. diff --git a/.changeset/spotty-swans-run.md b/.changeset/spotty-swans-run.md new file mode 100644 index 0000000000..7a16343d69 --- /dev/null +++ b/.changeset/spotty-swans-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The backend development setup now ignores the `"browser"` and `"module"` entry points in `package.json`, and instead always uses `"main"`. diff --git a/.changeset/strong-beds-attack.md b/.changeset/strong-beds-attack.md new file mode 100644 index 0000000000..f16c709daa --- /dev/null +++ b/.changeset/strong-beds-attack.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-org': patch +--- + +Fixed EntityOwnerPicker and OwnershipCard url filter issue with more than 21 owners diff --git a/.changeset/strong-pandas-roll.md b/.changeset/strong-pandas-roll.md new file mode 100644 index 0000000000..e58e1c44c6 --- /dev/null +++ b/.changeset/strong-pandas-roll.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-periskop': minor +'@backstage/plugin-periskop-backend': minor +--- + +Add periskop and periskop-backend plugin, for usage with exception aggregation tool https://periskop.io/ diff --git a/.changeset/swift-mails-sing.md b/.changeset/swift-mails-sing.md new file mode 100644 index 0000000000..3df53e0340 --- /dev/null +++ b/.changeset/swift-mails-sing.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +The panels of `TechDocsCustomHome` now use the `useEntityOwnership` hook to resolve ownership when the `'ownedByUser'` filter predicate is used. diff --git a/.changeset/swift-rules-hunt.md b/.changeset/swift-rules-hunt.md deleted file mode 100644 index 15707bbf90..0000000000 --- a/.changeset/swift-rules-hunt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -adds passing projectID to the Storage client diff --git a/.changeset/tasty-carpets-fold.md b/.changeset/tasty-carpets-fold.md new file mode 100644 index 0000000000..a1e62413bc --- /dev/null +++ b/.changeset/tasty-carpets-fold.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed the `useEntityKinds` hook, use `catalogApi.getEntityFacets({ facets: ['kind'] })` instead. diff --git a/.changeset/techdocs-byta-namnet.md b/.changeset/techdocs-byta-namnet.md new file mode 100644 index 0000000000..f77849513b --- /dev/null +++ b/.changeset/techdocs-byta-namnet.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-node': patch +--- + +Renamed `@backstage/techdocs-common` to `@backstage/plugin-techdocs-node`. diff --git a/.changeset/techdocs-cats-whisper.md b/.changeset/techdocs-cats-whisper.md deleted file mode 100644 index 290f8a1d7a..0000000000 --- a/.changeset/techdocs-cats-whisper.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Remove copyright from old footer in documentation generated with previous version of `mkdocs-techdocs-plugin` (`v0.2.2`). diff --git a/.changeset/techdocs-empty-office.md b/.changeset/techdocs-empty-office.md new file mode 100644 index 0000000000..082521be35 --- /dev/null +++ b/.changeset/techdocs-empty-office.md @@ -0,0 +1,7 @@ +--- +'@backstage/techdocs-common': patch +--- + +**DEPRECATION** + +The `@backstage/techdocs-common` package is being renamed `@backstage/plugin-techdocs-node`. We may continue to publish changes to `@backstage/techdocs-common` for a time, but will stop doing so in the near future. If you depend on this package, you should update your dependencies to point at the renamed package. diff --git a/.changeset/techdocs-node-one.md b/.changeset/techdocs-node-one.md new file mode 100644 index 0000000000..706c708c4a --- /dev/null +++ b/.changeset/techdocs-node-one.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs-backend': patch +'@techdocs/cli': patch +--- + +Use `@backstage/plugin-techdocs-node` package instead of `@backstage/techdocs-common`. diff --git a/.changeset/techdocs-panthers-listen.md b/.changeset/techdocs-panthers-listen.md deleted file mode 100644 index 0ef28f6281..0000000000 --- a/.changeset/techdocs-panthers-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Show feedback when copying code snippet to clipboard. diff --git a/.changeset/techdocs-toes-lie.md b/.changeset/techdocs-toes-lie.md deleted file mode 100644 index 14ca2567e2..0000000000 --- a/.changeset/techdocs-toes-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@techdocs/cli': patch ---- - -Bump `@backstage/techdocs-common` to `0.11.10` to use `spotify/techdocs:v0.3.7` which upgrades `mkdocs-theme` as a dependency of `mkdocs-techdocs-core`. diff --git a/.changeset/techdocs-war-on-war.md b/.changeset/techdocs-war-on-war.md deleted file mode 100644 index 28722feed3..0000000000 --- a/.changeset/techdocs-war-on-war.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Fixed a bug that could cause searches in the in-context TechDocs search bar to show results from a different TechDocs site. diff --git a/.changeset/ten-queens-dance.md b/.changeset/ten-queens-dance.md new file mode 100644 index 0000000000..947675aa46 --- /dev/null +++ b/.changeset/ten-queens-dance.md @@ -0,0 +1,7 @@ +--- +'@backstage/backend-tasks': minor +--- + +**BREAKING**: The `TaskDefinition` type has been removed, and replaced by the equal pair `TaskScheduleDefinition` and `TaskInvocationDefinition`. The interface for `PluginTaskScheduler.scheduleTask` stays effectively unchanged, so this only affects you if you use the actual types directly. + +Added the method `PluginTaskScheduler.createTaskSchedule`, which returns a `TaskSchedule` wrapper that is convenient to pass down into classes that want to control their task invocations while the caller wants to retain control of the actual schedule chosen. diff --git a/.changeset/ten-rats-join.md b/.changeset/ten-rats-join.md new file mode 100644 index 0000000000..6aa97be581 --- /dev/null +++ b/.changeset/ten-rats-join.md @@ -0,0 +1,8 @@ +--- +'@backstage/catalog-model': patch +--- + +**DEPRECATION**: + +- Deprecated `CommonValidatorFunctions.isValidString`, please use `isNonEmptyString` instead which is equivalent but better named. +- Deprecated `CommonValidatorFunctions.isValidTag`, with no replacement. Its purpose was too specific and not reusable, so it will be removed. diff --git a/.changeset/tender-berries-lie.md b/.changeset/tender-berries-lie.md new file mode 100644 index 0000000000..74a3eb4707 --- /dev/null +++ b/.changeset/tender-berries-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Changed the logic for how modules are marked as external in the Rollup build of packages. Rather than only marking dependencies and build-in Node.js modules as external, all non-relative imports are now considered external. diff --git a/.changeset/thick-games-dress.md b/.changeset/thick-games-dress.md new file mode 100644 index 0000000000..5ce2decd3f --- /dev/null +++ b/.changeset/thick-games-dress.md @@ -0,0 +1,8 @@ +--- +'@backstage/backend-common': minor +--- + +**BREAKING**: + +- Removed the (since way back) deprecated `createDatabase` export, please use `createDatabaseClient` instead. +- Removed the (since way back) deprecated `SingleConnectionDatabaseManager` export, please use `DatabaseManager` instead. diff --git a/.changeset/thick-gifts-cheat.md b/.changeset/thick-gifts-cheat.md new file mode 100644 index 0000000000..00154a8492 --- /dev/null +++ b/.changeset/thick-gifts-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': minor +--- + +**BREAKING**: Removed previously deprecated `CatalogApi.getEntityByName`, please use `getEntityByRef` instead. diff --git a/.changeset/tiny-lobsters-exercise.md b/.changeset/tiny-lobsters-exercise.md deleted file mode 100644 index 78cc753bab..0000000000 --- a/.changeset/tiny-lobsters-exercise.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@backstage/plugin-techdocs': minor ---- - -**BREAKING:** -Table column utilities `createNameColumn`, `createOwnerColumn`, `createTypeColumn` as well as actions utilities `createCopyDocsUrlAction` and `createStarEntityAction` are no longer directly exported. Instead accessible through DocsTable and EntityListDocsTable. - -Use as following: - -```tsx -DocsTable.columns.createNameColumn(); -DocsTable.columns.createOwnerColumn(); -DocsTable.columns.createTypeColumn(); - -DocsTable.actions.createCopyDocsUrlAction(); -DocsTable.actions.createStarEntityAction(); -``` - -- Renamed `DocsResultListItem` to `TechDocsSearchResultListItem`, leaving the old name in place as a deprecations. - -- Renamed `TechDocsPage` to `TechDocsReaderPage`, leaving the old name in place as a deprecations. - -- Renamed `TechDocsPageRenderFunction` to `TechDocsPageRenderFunction`, leaving the old name in place as a deprecations. - -- Renamed `TechDocsPageHeader` to `TechDocsReaderPageHeader`, leaving the old name in place as a deprecations. - -- `LegacyTechDocsHome` marked as deprecated and will be deleted in next release, use `TechDocsCustomHome` instead. - -- `LegacyTechDocsPage` marked as deprecated and will be deleted in next release, use `TechDocsReaderPage` instead. diff --git a/.changeset/tough-clocks-enjoy.md b/.changeset/tough-clocks-enjoy.md deleted file mode 100644 index 90e00382df..0000000000 --- a/.changeset/tough-clocks-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Tweaked the wording of the "does not have a location" errors to include the actual missing annotation name, to help users better in fixing their inputs. diff --git a/.changeset/tough-pets-beg.md b/.changeset/tough-pets-beg.md deleted file mode 100644 index 7fdcc6dc01..0000000000 --- a/.changeset/tough-pets-beg.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': patch ---- - -Fix Bitbucket Cloud and Bitbucket Server line number reference. diff --git a/.changeset/twenty-birds-think.md b/.changeset/twenty-birds-think.md new file mode 100644 index 0000000000..5b1f9481c0 --- /dev/null +++ b/.changeset/twenty-birds-think.md @@ -0,0 +1,19 @@ +--- +'@backstage/create-app': patch +--- + +Updated template to use package roles. To apply this change to an existing app, check out the [migration guide](https://backstage.io/docs/tutorials/package-role-migration). + +Specifically the following scripts in the root `package.json` have also been updated: + +```diff +- "build": "lerna run build", ++ "build": "backstage-cli repo build --all", + +... + +- "lint": "lerna run lint --since origin/master --", +- "lint:all": "lerna run lint --", ++ "lint": "backstage-cli repo lint --since origin/master", ++ "lint:all": "backstage-cli repo lint", +``` diff --git a/.changeset/twenty-fireants-turn.md b/.changeset/twenty-fireants-turn.md new file mode 100644 index 0000000000..fd9dc17530 --- /dev/null +++ b/.changeset/twenty-fireants-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Allow passing more repo configuration for `publish:github` action diff --git a/.changeset/twenty-planes-dress.md b/.changeset/twenty-planes-dress.md new file mode 100644 index 0000000000..4d1a5dc1d1 --- /dev/null +++ b/.changeset/twenty-planes-dress.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-aws': patch +--- + +Added `AwsS3DiscoveryProcessor`, which was moved here from `@backstage/plugin-catalog-backend` where it previously resided. diff --git a/.changeset/two-forks-tell.md b/.changeset/two-forks-tell.md deleted file mode 100644 index dbb17a516c..0000000000 --- a/.changeset/two-forks-tell.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog': patch -'@backstage/plugin-org': patch ---- - -Removed usage of deprecated `getEntityMetadataViewUrl` and `getEntityMetadataEditUrl` helpers. diff --git a/.changeset/two-mails-boil.md b/.changeset/two-mails-boil.md new file mode 100644 index 0000000000..22651f1bd4 --- /dev/null +++ b/.changeset/two-mails-boil.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': minor +--- + +**BREAKING**: Removed `CatalogClient.getLocationByEntity` and `CatalogClient.getOriginLocationByEntity` which had previously been deprecated. Please use `CatalogApi.getLocationByRef` instead. Note that this only affects you if you were using `CatalogClient` (the class) directly, rather than `CatalogApi` (the interface), since it has been removed from the interface in an earlier release. diff --git a/.changeset/unlucky-queens-brush.md b/.changeset/unlucky-queens-brush.md deleted file mode 100644 index c080856246..0000000000 --- a/.changeset/unlucky-queens-brush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Added ability for SidebarSubmenuItem to handle external links correctly via the "to" prop diff --git a/.changeset/warm-bananas-behave.md b/.changeset/warm-bananas-behave.md new file mode 100644 index 0000000000..b1b6b4c8ec --- /dev/null +++ b/.changeset/warm-bananas-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The `--since` flag of repo commands now silently falls back to using the provided `ref` directly if no merge base is available. diff --git a/.changeset/weak-schools-wash.md b/.changeset/weak-schools-wash.md new file mode 100644 index 0000000000..86f3111d93 --- /dev/null +++ b/.changeset/weak-schools-wash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed the deprecated `useOwnUser` hook. Existing usage can be replaced with `identityApi.getBackstageIdentity()`, followed by a call to `catalogClient.getEntityByRef(identity.userEntityRef)`. diff --git a/.changeset/wise-foxes-confess.md b/.changeset/wise-foxes-confess.md deleted file mode 100644 index 8322b66555..0000000000 --- a/.changeset/wise-foxes-confess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Fix the support for custom defaultBranch values for Bitbucket Cloud at the `publish:bitbucket` scaffolder action. diff --git a/.changeset/young-feet-flow.md b/.changeset/young-feet-flow.md new file mode 100644 index 0000000000..91e55da1d8 --- /dev/null +++ b/.changeset/young-feet-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Internalized usage of `useOwnedEntities` hook. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1a0f827fb1..44c61f9348 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -46,7 +46,7 @@ /plugins/search-* @backstage/reviewers @backstage/techdocs-core /plugins/sonarqube @backstage/reviewers @backstage/sda-se-reviewers /plugins/techdocs @backstage/reviewers @backstage/techdocs-core -/plugins/techdocs-backend @backstage/reviewers @backstage/techdocs-core +/plugins/techdocs-* @backstage/reviewers @backstage/techdocs-core /tech-insights-backend @backstage/reviewers @xantier @iain-b /tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b /tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 6359493d4b..2614dd6b50 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -20,7 +20,6 @@ Autoscaling autoselect Avro aws -backrub backported backporting Bigtable @@ -39,8 +38,6 @@ Changesets chanwit Chanwit ci -cisphobia -cissexist classname cli cloudbuild @@ -68,7 +65,6 @@ css Datadog dataflow dayjs -deadnaming debounce Debounce declaratively @@ -88,6 +84,7 @@ dockerfiles Dockerize dockerode Docusaurus +ecco env Env elasticsearch @@ -106,6 +103,7 @@ Firestore Fiverr gitbeaker GitHub +gitlab GitLab Gource Grafana @@ -171,7 +169,6 @@ Minikube Minio misconfiguration misconfigured -misgendering mkdocs Mkdocs monorepo @@ -213,6 +210,9 @@ parallelization Patrik Peloton performant +Performant +periskop +Periskop plantuml Platformize Podman diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49c97bb197..1f0c6e7602 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,7 +119,7 @@ jobs: run: yarn backstage-cli config:check --lax - name: lint - run: yarn lerna -- run lint --since origin/master + run: yarn backstage-cli repo lint --since origin/master - name: type checking and declarations run: yarn tsc:full @@ -139,11 +139,11 @@ jobs: - name: build changed packages if: ${{ steps.yarn-lock.outcome == 'success' }} - run: yarn lerna -- run build --since origin/master --include-dependencies + run: yarn backstage-cli repo build --all --since origin/master - name: build all packages if: ${{ steps.yarn-lock.outcome == 'failure' }} - run: yarn lerna -- run build + run: yarn backstage-cli repo build --all - name: verify type dependencies run: yarn lint:type-deps diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index 4997668563..ab4c9bcfdb 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -56,7 +56,7 @@ jobs: run: yarn tsc - name: build - run: yarn build + run: yarn backstage-cli repo build # Prepares a nightly release version of any package with pending changesets # Pre-mode is exited if case we're in it, otherwise it has no effect diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index 9b7a233767..fd9ed216be 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -102,13 +102,13 @@ jobs: run: yarn backstage-cli config:check --lax - name: lint - run: yarn lerna -- run lint + run: yarn backstage-cli repo lint - name: type checking and declarations run: yarn tsc:full - name: build - run: yarn build + run: yarn backstage-cli repo build --all - name: verify type dependencies run: yarn lint:type-deps @@ -188,7 +188,11 @@ jobs: run: yarn tsc:full - name: build packages - run: yarn lerna -- run --ignore example-app --ignore example-backend build + run: yarn backstage-cli repo build + + - name: build embedded techdocs app + working-directory: packages/techdocs-cli-embedded-app + run: yarn build # Publishes current version of packages that are not already present in the registry - name: publish diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index f4d9afc0df..328c64fb58 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -56,18 +56,7 @@ jobs: github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} # TODO(Rugvip): Remove the create-app dispatch once we've been on the release version for a while script: | - console.log('Dispatching upgrade helper sync - release version'); - await github.rest.actions.createWorkflowDispatch({ - owner: 'backstage', - repo: 'upgrade-helper-diff', - workflow_id: 'release.yml', - ref: 'master', - inputs: { - version: require('./backstage/package.json').version, - }, - }); - - console.log('Dispatching upgrade helper sync - create-app version'); + console.log('Dispatching upgrade helper sync'); await github.rest.actions.createWorkflowDispatch({ owner: 'backstage', repo: 'upgrade-helper-diff', @@ -75,5 +64,6 @@ jobs: ref: 'master', inputs: { version: require('./backstage/packages/create-app/package.json').version, + releaseVersion: require('./backstage/package.json').version }, }); diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index 3e39aee1ae..e333aaba0c 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -71,8 +71,7 @@ jobs: # End of yarn setup - run: yarn tsc - - name: yarn build - run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli + - run: yarn backstage-cli repo build - name: run E2E test run: | sudo sysctl fs.inotify.max_user_watches=524288 diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 0977831d19..bc6bc8a698 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -50,8 +50,7 @@ jobs: run: yarn install --frozen-lockfile - run: yarn tsc - - name: yarn build - run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli + - run: yarn backstage-cli repo build - name: run E2E test run: yarn e2e-test run env: diff --git a/.github/workflows/verify_windows.yml b/.github/workflows/verify_windows.yml index 9ae4397bba..5326e293d3 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.yml @@ -47,7 +47,7 @@ jobs: # End of yarn setup - name: lint - run: yarn lerna -- run lint + run: yarn backstage-cli repo lint - name: type checking and declarations run: yarn tsc:full diff --git a/ADOPTERS.md b/ADOPTERS.md index eb83726b8d..1961054237 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -99,4 +99,8 @@ _If you're using Backstage in your organization, please try to add your company | [Procore](https://www.procore.com/jobs/engineering) | [@shayon](https://github.com/Shayon), [@jamesdabbs-procore](https://github.com/jamesdabbs-procore) | Developer portal - centralized software catalog and templates to enable self serve and reduce cognitive load. | | [Bradesco](https://banco.bradesco/html/classic/sobre/index.shtm) | [@danilosoarescardoso](https://github.com/danilosoarescardoso) | Centralized developer portal with software catalog, software templates, techdocs and some plugins from community and by our own. | | [SeatGeek](https://seatgeek.com) | [@zhammer](https://github.com/zhammer) | Software catalog and documentation platform | -| [Grupo Boticário](https://www.grupoboticario.com.br/) | [@chicoribas](https://github.com/chicoribas), [@fndiaz](https://github.com/fndiaz), [@digogid](https://github.com/digogid), [@manumbs](https://github.com/manumbs), [@haooliveira84](https://github.com/haooliveira84), [@Rhullyam](https://github.com/Rhullyam) | Centralized developer portal with catalog, documentation, and software templates to build a ready to go application, including pipelines (CI/CD) and cloud services provisioning | \ No newline at end of file +| [Grupo Boticário](https://www.grupoboticario.com.br/) | [@chicoribas](https://github.com/chicoribas), [@fndiaz](https://github.com/fndiaz), [@digogid](https://github.com/digogid), [@manumbs](https://github.com/manumbs), [@haooliveira84](https://github.com/haooliveira84), [@Rhullyam](https://github.com/Rhullyam) | Centralized developer portal with catalog, documentation, and software templates to build a ready to go application, including pipelines (CI/CD) and cloud services provisioning | +| [Snyk](https://snyk.io/) | [@robcresswell](https://github.com/robcresswell) | Developer portal for software discovery, API documentation and templating | +| [Stilingue](https://www.stilingue.com.br/) | [@stilingue-inteligencia-artificial](https://github.com/Stilingue-IA), [@bbviana](https://github.com/bbviana) | Developer portal, services catalog and centralization of metrics from Grafana, Sentry and GCP. Furthermore, centralization of documentation and infra details like DNS, Network services, SSL and so on. | +| [TUI Group](https://www.tuigroup.com/) | [Simon Stamm](https://github.com/simonstamm), [Christian Rudolph](https://github.com/ChrisRu82) | Developer portal for all engineer to provide discoverability to all internal components, APIs, documentation and to scaffold templates with integrations to our internal tools extended by plugins from our community. | +| [Alliander](https://www.alliander.com/) | [@leon-vg](https://github.com/leon-vg), [@gieljl](https://github.com/gieljl), [@niekteg](https://github.com/niekteg) | Developer portal - software catalog, technical documentation, software templates, tech radar and exploration of used tools/services | diff --git a/README.md b/README.md index 1078631926..e78800ef84 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ # [Backstage](https://backstage.io) +_During March 7 to March 11 the maintainers will be taking part in Spotify's annual hack week. Development will continue as usual, but expect a slower pace for discussions and PR reviews. Why not take this opportunity to [build a plugin](https://backstage.io/docs/plugins/)?_ + [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![CNCF Status](https://img.shields.io/badge/cncf%20status-sandbox-blue.svg)](https://www.cncf.io/projects) [![Main CI Build](https://github.com/backstage/backstage/workflows/Main%20Master%20Build/badge.svg)](https://github.com/backstage/backstage/actions?query=workflow%3A%22Main+Master+Build%22) diff --git a/SECURITY.md b/SECURITY.md index ba96c694ee..49e26349b0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -36,3 +36,79 @@ There are many situations where a vulnerability does not affect a particular dep To work around this and other similar issues, Snyk provides a method to ignore vulnerabilities. In order to provide the best visibility and most utility to adopters of Backstage, we store these ignore rules in `.snyk` policy files. This allows adopters to rely on our ignore policies if they wish to do so. Adding a new ignore policy is done by creating or modifying an existing `.snyk` file within a package root. See the [Snyk Documentation](https://support.snyk.io/hc/en-us/articles/360007487097-The-snyk-file) for details on the syntax. Always include a description, full path, and time limit of the ignore policy. + +## Coding Practices + +In this section we highlight patterns where particular care needs to be taken in order to avoid security vulnerabilities, as well as the best practices to follow. + +### Local file path resolution + +A common pitfall in backend packages is to resolve local file paths based on user input, without sufficiently protecting against input that traverses outside the intended directory. + +For example, consider the following code: + +```ts +// WARNING: DO NOT DO THIS + +import { join } from 'path'; +import fs from 'fs-extra'; + +function writeTemporaryFile(tmpDir: string, name: string, content: string) { + const filePath = join(tmpDir, name); + await fs.writeFile(filePath, content); +} +``` + +If the `name` of the file is controlled by the user, they can for example enter `../../../../etc/hosts` as the name of the file. This can lead to a file being written outside the intended directory, which in turn can be used to inject malicious code or other form of attacks. + +The recommended solution to this is to use `resolveSafeChildPath` from `@backstage/backend-common` to resolve the file path instead. It makes sure that the resolved path does not fall outside the provided directory. If you simply want to validate whether a file path is safe, you can use `isChildPath` instead. + +The insecure example above should instead be written like this: + +```ts +// THIS IS GOOD, DO THIS + +import { resolveSafeChildPath } from '@backstaghe/backend-common'; +import fs from 'fs-extra'; + +function writeTemporaryFile(tmpDir: string, name: string, content: string) { + const filePath = resolveSafeChildPath(tmpDir, name); + await fs.writeFile(filePath, content); +} +``` + +### Express responses + +When returning a response from an Express route, always use `.json(...)` or `.end()` without any arguments. This ensures that the response can not be interpreted as an HTML document with embedded JavaScript by the browser. Never use `.send(...)` unless you want to send other forms of content, and be sure to always set an explicit content type. If you need to return HTML or other content that may be executed by the browser, be very careful how you handle user input. + +If you want to return an error response, simply throw the error or pass it on to the `next(...)` callback. There is a middleware installed that will transform the error into a JSON response. Many of the common HTTP errors are available from `@backstage/errors`, for example `NotFoundError`, which will also set the correct status code. + +The following example show how to return an error that contains user input: + +```ts +res.send(`Invalid id: '${req.params.id}'`); // BAD + +// import { InputError } from '@backstage/errors'; +throw new InputError(`Invalid id: '${req.params.id}'`); // GOOD + +// OR, in case a custom response is needed +res.json({ message: `Invalid id: '${req.params.id}'` }); // NOT BAD +``` + +No matter how trivial it may seem, always use `.json(...)`. It reduces the risk that a future refactoring introduces vulnerabilities: + +```ts +res.send({ ok: true }); // BAD + +res.json({ ok: true }); // GOOD +``` + +If you absolutely must return a string with `.send(...)`, use an explicit and secure `Content-Type`: + +```ts +res.send(`message=${message}`); // BAD + +res.contentType('text/plain').send(`message=${message}`); // GOOD +``` + +An example of how to return dynamic HTML is not provided here. If you need to do so, proceed with extreme caution and be very sure that you know what you are doing. diff --git a/contrib/search/README.md b/contrib/search/README.md new file mode 100644 index 0000000000..9a43a05c4c --- /dev/null +++ b/contrib/search/README.md @@ -0,0 +1,3 @@ +# Search + +Contributions/extensions to the Search plugin diff --git a/contrib/search/confluence/ConfluenceCollator.md b/contrib/search/confluence/ConfluenceCollator.md new file mode 100644 index 0000000000..f636ae88ea --- /dev/null +++ b/contrib/search/confluence/ConfluenceCollator.md @@ -0,0 +1,95 @@ +ConfluenceCollator.ts reference + +```ts +import { DocumentCollator } from '@backstage/search-common'; +import fetch from 'cross-fetch'; + +export class ConfluenceCollator implements DocumentCollator { + public readonly type: string = 'confluence'; + + async execute() { + const ConfluenceUrlBase = + 'https://{CONFLUENCE-ORG-NAME}.atlassian.net/wiki/rest/api'; + + async function getConfluenceData(requestUrl: string) { + var emptyJson = {}; + try { + const res = await fetch(requestUrl, { + method: 'get', + headers: { + Authorization: `Basic ${process.env.CONFLUENCE_TOKEN}`, + }, + }); + if (res.ok) { + return await res.json(); + } + } catch (err) { + console.error(err); + } + return emptyJson; + } + + async function getSpaces(): Promise { + const data = await getConfluenceData( + `${ConfluenceUrlBase}/space?&limit=1000&type=global&status=current`, + ); + let spacesList = []; + if (data['results']) { + const results = data['results']; + for (const result of results) { + spacesList.push(result['key']); + } + } + return spacesList; + } + + async function getDocumentsFromSpaces(spaces: string[]): Promise { + let documentsList = []; + for (var space of spaces) { + let next = true; + let requestUrl = `${ConfluenceUrlBase}/content?limit=1000&status=current&spaceKey=${space}`; + while (next) { + const data = await getConfluenceData(requestUrl); + if (data['results']) { + const results = data['results']; + for (const result of results) { + documentsList.push(result['_links']['self']); + } + if (data['_links']['next']) { + requestUrl = data['_links']['base'] + data['_links']['next']; + } else { + next = false; + } + } else { + break; + } + } + } + return documentsList; + } + + async function getDocumentInfo(documents: string[]) { + let documentInfo = []; + for (var documentUrl of documents) { + const data = await getConfluenceData( + documentUrl + '?expand=body.storage', + ); + if (data['status'] && data['status'] == 'current') { + const documentMetaData = { + title: data['title'], + text: data['body']['storage']['value'], + location: data['_links']['base'] + data['_links']['webui'], + }; + documentInfo.push(documentMetaData); + } + } + return documentInfo; + } + + const spacesList = await getSpaces(); + const documentsList = await getDocumentsFromSpaces(spacesList); + const documentMetaDataList = await getDocumentInfo(documentsList); + return documentMetaDataList; + } +} +``` diff --git a/contrib/search/confluence/ConfluenceResultListItem.md b/contrib/search/confluence/ConfluenceResultListItem.md new file mode 100644 index 0000000000..ba7c4f91a9 --- /dev/null +++ b/contrib/search/confluence/ConfluenceResultListItem.md @@ -0,0 +1,59 @@ +ConfluenceResultListItem.tsx reference + +```tsx +import React from 'react'; +import { Link } from '@backstage/core-components'; +import { IndexableDocument } from '@backstage/search-common'; +import { + Divider, + ListItem, + ListItemIcon, + ListItemText, +} from '@material-ui/core'; + +type Props = { + result: IndexableDocument; +}; + +export const ConfluenceResultListItem = ({ result }: Props) => { + // Remove html tags from document text before displaying + const chars = []; + let isTag = false; + for (const c of result.text.substring(0, 500)) { + if (c === '<') { + isTag = true; + continue; + } + if (c === '>') { + isTag = false; + chars.push(' '); + continue; + } + if (!isTag) { + chars.push(c); + } + } + const excerpt = + chars.join('').substring(0, 80) + (result.text.length > 80 ? '...' : ''); + + return ( + + + + + + + + + + ); +}; +``` diff --git a/contrib/search/confluence/README.md b/contrib/search/confluence/README.md new file mode 100644 index 0000000000..80beb9c169 --- /dev/null +++ b/contrib/search/confluence/README.md @@ -0,0 +1,32 @@ +# Confluence + +These files help you add Confluence as a source to the Backstage Search plugin. +To do so, add both files in this directory under the packages/backend/src/plugins/search/ pathway in your Backstage app. +Then, add the following code to your packages/app/src/components/search/SearchPage.tsx: + +```tsx +import { ConfluenceResultListItem } from './ConfluenceResultListItem'; +``` + +```tsx +case 'confluence': + return ( + + ); +``` + +and the following to packages/backend/src/plugins/search.ts: + +```ts +import { ConfluenceCollator } from './search/ConfluenceCollator'; +``` + +```ts +indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + collator: new ConfluenceCollator(), +}); +``` diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index a33351e938..3b355470d1 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -27,6 +27,8 @@ sign-in resolvers and set them for any of the Authentication providers inside `@backstage/plugin-auth-backend` plugin. ```ts +import { DEFAULT_NAMESPACE, stringifyEntityRef } from '@backstage/catalog-model'; + export default async function createPlugin({ ... }: PluginEnvironment): Promise { @@ -38,22 +40,31 @@ export default async function createPlugin({ resolver: async ({ profile: { email } }, ctx) => { // Call a custom validator function that checks that the email is // valid and on our own company's domain, and throws an Error if it - // isn't + // isn't. + // TODO: Implement this function validateEmail(email); // List of entity references that denote the identity and // membership of the user - const ent = []; + const ent: string[] = []; // Let's use the username in the email ID as the user's default // unique identifier inside Backstage. const [id] = email.split('@'); - ent.push(`User:default/${id}`) + ent.push(stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: id, + })); // Let's call the internal LDAP provider to get a list of groups // that the user belongs to, and add those to the list as well const ldapGroups = await getLdapGroups(email); - ldapGroups.forEach(group => ent.push(`Group:default/${group}`)) + ldapGroups.forEach(group => ent.push(stringifyEntityRef({ + kind: 'Group', + namespace: DEFAULT_NAMESPACE, + name: group, + }))); // Issue the token containing the entity claims const token = await ctx.tokenIssuer.issueToken({ diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index 36967f352a..0b99e81d64 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -167,7 +167,9 @@ RUN yarn install --frozen-lockfile --network-timeout 600000 && rm -rf "$(yarn ca COPY . . RUN yarn tsc -RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies +RUN yarn --cwd packages/backend build +# If you have not yet migrated to package roles, use the following command instead: +# RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies # Stage 3 - Build the actual backend image and install production dependencies FROM node:16-bullseye-slim diff --git a/docs/features/search/README.md b/docs/features/search/README.md index 0856451882..ed950d8ac8 100644 --- a/docs/features/search/README.md +++ b/docs/features/search/README.md @@ -24,12 +24,12 @@ Backstage ecosystem. ## Project roadmap -| Version | Description | -| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Backstage Search Pre-Alpha ✅ | Search Frontend letting you search through the entities of the software catalog. [See Pre-Alpha Use Cases.](#backstage-search-pre-alpha) | -| Backstage Search Alpha ✅ | Basic “out-of-the-box” in-memory indexing process of entities, and their metadata, registered to the Software Catalog. [See Alpha Use Cases](#backstage-search-alpha). | -| [Backstage Search Beta ⌛][beta] | At least one production-ready search engine that supports the same use-cases as in the alpha. [See Beta Use Cases](#backstage-search-beta). | -| [Backstage Search GA ⌛][ga] | A stable Search API for plugin developers to add search to their plugins, and app integrators to expose that to their users. [See GA Use Cases](#backstage-search-ga). | +| Version | Description | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Backstage Search Pre-Alpha ✅ | Search Frontend letting you search through the entities of the software catalog. [See Pre-Alpha Use Cases.](#backstage-search-pre-alpha) | +| Backstage Search Alpha ✅ | Basic “out-of-the-box” in-memory indexing process of entities, and their metadata, registered to the Software Catalog. [See Alpha Use Cases](#backstage-search-alpha). | +| Backstage Search Beta ✅ | At least one production-ready search engine that supports the same use-cases as in the alpha. [See Beta Use Cases](#backstage-search-beta). | +| [Backstage Search GA ⌛][ga] | A stable Search API for plugin developers to add search to their plugins, and app integrators to expose that to their users. [See GA Use Cases](#backstage-search-ga). | [beta]: https://github.com/backstage/backstage/milestone/27 [ga]: https://github.com/backstage/backstage/milestone/28 @@ -128,7 +128,7 @@ plugins integrated to search. | Frontend Plugin | @backstage/plugin-search | | Backend Plugin | @backstage/plugin-search-backend | | Indexer Plugin | @backstage/plugin-search-backend-node | -| Common Code | @backstage/search-common | +| Common Code | @backstage/plugin-search-common | ## Get Involved diff --git a/docs/features/search/concepts.md b/docs/features/search/concepts.md index 8cb17b12e6..052f71376f 100644 --- a/docs/features/search/concepts.md +++ b/docs/features/search/concepts.md @@ -54,13 +54,14 @@ An index is a collection of such documents of a given type. ### Collators You need to be able to search something! Collators are the way to define what -can be searched. Specifically, they're classes which return documents conforming -to a minimum set of fields (including a document title, location, and text), but -which can contain any other fields as defined by the collator itself. One -collator is responsible for defining and collecting documents of a type. +can be searched. Specifically, they're readable object streams of documents that +conform to a minimum set of fields (including a document title, location, and +text), but which can contain any other fields as defined by the collator itself. +One collator is responsible for defining and collecting documents of a type. -Some plugins, like the Catalog Backend, provide so-called "default" collators -which you can use out-of-the-box to start searching across Backstage quickly. +Some plugins, like the Catalog Backend, provide so-called "default" collator +factories which you can use out-of-the-box to start searching across Backstage +quickly. ### Decorators @@ -68,9 +69,15 @@ Sometimes you want to add extra information to a set of documents in your search index that the collator may not be aware of. For example, the Software Catalog knows about software entities, but it may not know about their usage or quality. -Decorators are classes which can add extra fields to pre-collated documents. -This extra metadata could then be used to bias search results or otherwise -improve the search experience in your Backstage instance. +Decorators are transform streams which sit between a collator (read stream) and +an indexer (write stream) during the indexing process. It can be used to add +extra fields to documents as they are being collated and indexed. This extra +metadata could then be used to bias search results or otherwise improve the +search experience in your Backstage instance. + +In addition to adding extra metadata, decorators (like any transform stream) can +also be used to remove metadata, filter out, or even add extra documents at +index-time. ### The Scheduler diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index a3c42c682c..c41e9c7bdc 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -48,10 +48,10 @@ const app = createApp({ ## How to index TechDocs documents The TechDocs plugin has supported integrations to Search, meaning that it -provides a default collator ready to be used. +provides a default collator factory ready to be used. The purpose of this guide is to walk you through how to register the -[DefaultTechDocsCollator](https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts) +[DefaultTechDocsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts) in your App, so that you can get TechDocs documents indexed. If you have been through the @@ -60,18 +60,19 @@ you should have the `packages/backend/src/plugins/search.ts` file available. If so, you can go ahead and follow this guide - if not, start by going through the getting started guide. -1. Import the DefaultTechDocsCollator from `@backstage/plugin-techdocs-backend`. +1. Import the `DefaultTechDocsCollatorFactory` from + `@backstage/plugin-techdocs-backend`. ```typescript -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; ``` -2. Register the DefaultTechDocsCollator with the IndexBuilder. +2. Register the `DefaultTechDocsCollatorFactory` with the IndexBuilder. ```typescript indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultTechDocsCollator.fromConfig(config, { + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { discovery, logger, tokenManager, @@ -131,3 +132,268 @@ indexBuilder.addCollator({ As shown above, you can add a catalog entity filter to narrow down what catalog entities are indexed by the search engine. + +## How to migrate from Search Alpha to Beta + +For the purposes of this guide, Search Beta version is defined as: + +- **Search Plugin**: At least `v0.7.2` +- **Search Backend Plugin**: At least `v0.4.6` +- **Search Backend Node**: At least `v0.5.0` +- **Search Common**: At least `v0.3.0` + +In the Beta version, the Search Platform's indexing process has been rewritten +as a stream pipeline in order to improve efficiency and performance on large +sets of documents. + +If you've not yet extended the Search Platform with custom code, and have +instead taken advantage of default collators, decorators, and search engines +provided by existing plugins, the migration process is fairly straightforward: + +1. Upgrade to at least version `0.5.0` of + `@backstage/plugin-search-backend-node`, as well as any backend plugins whose + collators you are using (e.g. at least version `0.23.0` of + `@backstage/plugin-catalog-backend` and/or version `0.14.1` of + `@backstage/plugin-techdocs-backend`), as well as any search-engine specific + plugin you are using (e.g. at least version `0.3.0` of + `@backstage/plugin-search-backend-module-pg` or version `0.1.0` of + `@backstage/plugin-search-backend-module-elasticsearch`). +2. Then, make the following changes to your + `/packages/backend/src/plugins/search.ts` file: + + ```diff + -import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; + -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; + +import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; + +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; + // ... + const indexBuilder = new IndexBuilder({ logger, searchEngine }); + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + - collator: DefaultCatalogCollator.fromConfig(config, { discovery }), + + factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery }), + }); + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + - collator: DefaultTechDocsCollator.fromConfig(config, { + + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { + discovery, + logger, + }), + }); + ``` + +Any custom collators, decorators, or search engine implementations will require +minor refactoring. Continue on for details. + +### Rewriting alpha-style collators for beta + +In alpha versions of the Backstage Search Platform, collators were classes that +implemented an `execute` method which resolved an `IndexableDocument` array. + +In beta versions, the logic encapsulated by the aforementioned `execute` method +is contained within an [object-mode][obj-mode] `Readable` stream where each +object pushed onto the stream is of type `IndexableDocument`. Instances of this +stream are instantiated by a factory class conforming to the +`DocumentCollatorFactory` interface. + +The optimal conversion strategy will vary depending on the collator's logic, but +the simplest conversion can follow a process like this: + +1. Rename your collator class to something like `YourCollatorFactory` and update + it to implement `DocumentCollatorFactory` instead of `DocumentCollator`. +2. Update its `execute` method so that it resolves + `AsyncGenerator` instead of `YourIndexableDocument[]`. +3. Implement `DocumentCollatorFactory`'s `getCollator` method which resolves to + `Readable.from(this.execute())` (which is a utility for creating [readable + streams][read-stream] from [async generators][async-gen]). + +```ts +import { DocumentCollatorFactory } from '@backstage/plugin-search-backend-node'; +import { Readable } from 'stream'; +export class YourCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'your-type'; + async *execute(): AsyncGenerator { + const widgets = await this.client.getWidgets(); + for (const widget of widgets) { + yield { + title: widget.name, + location: widget.url, + text: widget.description, + }; + } + } + async getCollator() { + return Readable.from(this.execute()); + } +} +``` + +Note: it may be possible to simplify your collator dramatically! If your custom +collator was previously using streams under the hood (for example, by reading +newline delimited JSON from a local or remote file), you could just expose the +stream directly via a simple factory class: + +```ts +import { DocumentCollatorFactory } from '@backstage/plugin-search-backend-node'; +import { createReadStream } from 'fs'; +import { parse } from '@jsonlines/core'; +export class YourCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'your-type'; + async getCollator() { + const parseStream = parse(); + return createReadStream('./documents.ndjson').pipe(parseStream); + } +} +``` + +### Rewriting alpha-style decorators for beta + +In alpha versions of the Backstage Search Platform, decorators were classes that +implemented an `execute` method which took an `IndexableDocument` array as an +argument, and resolved a modified array of the same type. + +In beta versions, the logic encapsulated by the aforementioned `execute` method +is contained within an object-mode `Transform` stream which reads objects of +type `IndexableDocument`, and writes objects of a conforming type. Similar to +collators, instances of this stream are instantiated by a factory class +conforming to the `DocumentDecoratorFactory` interface. + +Although you can choose to implement a `Transform` stream from scratch, the +`@backstage/plugin-search-backend-node` package provides a `DecoratorBase` class +in order to simplify the developer experience. With this base class, all that's +needed is to transfer your old decorator class logic into the base class' three +methods (`initialize`, `decorate`, and `finalize`), and implement the factory +class that instantiates the stream: + +```ts +import { DecoratorBase } from '@backstage/plugin-search-backend-node'; +export class YourDecorator extends DecoratorBase { + async initialize() { + // Setup logic. Performed once before any documents are consumed. + } + async decorate( + document: YourIndexableDocument, + ): Promise { + // Perform transformation logic here. + return document; + } + async finalize() { + // Teardown logic. Performed once after all documents have been consumed. + } +} +export class YourDecoratorFactory implements DocumentDecoratorFactory { + async getDecorator() { + return new YourDecorator(); + } +} +``` + +Note the return type of the `decorate` method and how each can be used to +different effect. + +- By resolving a single `YourIndexableDocument` object, your decorator can be + used to make simple transformations: + + ```ts + class BooleanWidgetCoolnessDecorator extends DecoratorBase { + async decorator(widget) { + // Perform a simple, 1:1 transformation. + widget.isCool = widget.isCool === 'true' ? true : false; + return widget; + } + } + ``` + +- By resolving `undefined`, your decorator can filter out documents which + shouldn't be in the index: + + ```ts + class OnlyCoolWidgetsDecorator extends DecoratorBase { + async decorator(widget) { + // Perform a simple filter operation. + return widget.isCool ? widget : undefined; + } + } + ``` + +- By resolving an array of `YourIndexableDocument` objects, you can generate + multiple documents based on the content of one: + + ```ts + class WidgetByVariantDecorator extends DecoratorBase { + async decorator(widget) { + // Generate one widget doc per widget variant. + return widget.variants.map(variant => { + // Each widget doc is the given widget plus a "variant" property + // pulled from a widget.variants string array. + return { + ...widget, + variant, + }; + }); + } + } + ``` + +In alpha versions, a decorator had access to every `IndexableDocument` +simultaneously. This is no longer possible in beta versions (precisely to make +the indexing process more efficient and performant). You will need to modify +your decorator's logic so that it does not need access to every document at +once. + +### Rewriting alpha-style search engines for beta + +Search Engines are responsible for both querying and indexing documents to an +underlying search engine technology. While the search engine query interface +didn't change between alpha and beta versions, the indexing half of the +interface _did_ change. + +In alpha versions of the Backstage Search Platform, a search engine implemented +an `index` method which took a `type` and an `IndexableDocument` array and was +responsible for writing these documents to the underlying search engine. + +In beta versions, the logic encapsulated by the aforementioned `index` method is +contained within an object-mode `Writable` stream which expects objects of type +`IndexableDocument`. On the search engine class itself, the `index` method is +replaced with a `getIndexer` factory method which still takes the `type`, but +resolves an instance of the aforementioned `Writable` stream. + +Although you can choose to implement a `Writable` stream from scratch, the +`@backstage/plugin-search-backend-node` package provides a +`BatchSearchEngineIndexer` class in order to simplify the developer experience. +With this base class, which collects documents in batches of a configurable size +on your behalf, all that's needed is to transfer your old `index` method logic +into the base class' three methods (`initialize`, `index`, and `finalize`), and +implement the factory method that instantiates the stream: + +```ts +import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; +import { SearchEngine } from '@backstage/plugin-search-common'; +export class YourSearchEngineIndexer extends BatchSearchEngineIndexer { + constructor({ type }: { type: string }) { + // Customize the number of documents passed to the index method per batch. + super({ batchSize: 500 }); + // An imaginary search engine indexing client. + this.index = new SomeSearchEngineIndex({ indexName: type }); + } + async initialize() { + // Setup logic. Performed once before any documents are consumed. + } + async index(documents: IndexableDocument[]) { + await this.index.batchOf(documents); + } + async finalize() { + // Teardown logic. Performed once after all documents have been consumed. + } +} +export class YourSearchEngine implements SearchEngine { + async getIndexer(type: string) { + return new YourSearchEngineIndexer({ type }); + } +} +``` + +[obj-mode]: https://nodejs.org/docs/latest-v14.x/api/stream.html#stream_object_mode +[read-stream]: https://nodejs.org/docs/latest-v14.x/api/stream.html#stream_readable_streams +[async-gen]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of#iterating_over_async_generators diff --git a/docs/features/software-catalog/external-integrations.md b/docs/features/software-catalog/external-integrations.md index 59caf9475c..9f592e7cca 100644 --- a/docs/features/software-catalog/external-integrations.md +++ b/docs/features/software-catalog/external-integrations.md @@ -57,7 +57,7 @@ The recommended way of instantiating the catalog backend classes is to use the `CatalogBuilder`, as illustrated in the [example backend here](https://github.com/backstage/backstage/blob/master/packages/backend/src/plugins/catalog.ts). We will create a new -[`EntityProvider`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/providers/types.ts) +[`EntityProvider`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/api/provider.ts) subclass that can be added to this catalog builder. Let's make a simple provider that can refresh a set of entities based on a @@ -355,7 +355,7 @@ The recommended way of instantiating the catalog backend classes is to use the `CatalogBuilder`, as illustrated in the [example backend here](https://github.com/backstage/backstage/blob/master/packages/backend/src/plugins/catalog.ts). We will create a new -[`CatalogProcessor`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/ingestion/processors/types.ts) +[`CatalogProcessor`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/api/processor.ts) subclass that can be added to this catalog builder. It is up to you where you put the code for this new processor class. For quick diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 26a8710820..4fa71329dd 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -222,6 +222,20 @@ definition. Specifying this annotation will enable GoCD related features in Backstage for that entity. +### periskop.io/service-name + +```yaml +# Example: +metadata: + annotations: + periskop.io/service-name: pump-station +``` + +The value of this annotation is the periskop project name for the given entity. + +Specifying this annotation will enable [Periskop](https://periskop.io/) related features in Backstage for +that entity if the periskop plugin is installed. + ### sentry.io/project-slug ```yaml diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index ed7a103bc4..db765fc7ee 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -94,7 +94,7 @@ for example: catalog: locations: - type: url - target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml + target: https://github.com/backstage/software-templates/blob/main/scaffolder-templates/react-ssr-template/template.yaml rules: - allow: [Template] ``` diff --git a/docs/features/software-templates/builtin-actions.md b/docs/features/software-templates/builtin-actions.md index af3ae97b21..c8f830f3ee 100644 --- a/docs/features/software-templates/builtin-actions.md +++ b/docs/features/software-templates/builtin-actions.md @@ -15,13 +15,13 @@ A list of all registered actions can be found under `/create/actions`. For local development you should be able to reach them at `http://localhost:3000/create/actions`. -### Migrating from `fetch:cookiecutter` to `fetch:template` +## Migrating from `fetch:cookiecutter` to `fetch:template` The `fetch:template` action is a new action with a similar API to `fetch:cookiecutter` but no dependency on `cookiecutter`. There are two options for migrating templates that use `fetch:cookiecutter` to use `fetch:template`: -#### Using `cookiecutterCompat` mode +### Using `cookiecutterCompat` mode The new `fetch:template` action has a `cookiecutterCompat` flag which should allow most templates built for `fetch:cookiecutter` to work without any changes. @@ -43,7 +43,7 @@ allow most templates built for `fetch:cookiecutter` to work without any changes. values: ``` -#### Manual migration +### Manual migration If you prefer, you can manually migrate your templates to avoid the need for enabling cookiecutter compatibility mode, which will result in slightly less diff --git a/docs/features/software-templates/configuration.md b/docs/features/software-templates/configuration.md index d22b82bc16..87c17ca1a3 100644 --- a/docs/features/software-templates/configuration.md +++ b/docs/features/software-templates/configuration.md @@ -18,7 +18,7 @@ The next step is to add [add templates](http://backstage.io/docs/features/software-templates/adding-templates) to your Backstage app. -### Publishing defaults +## Publishing defaults Software templates can define _publish_ actions, such as `publish:github`, to create new repositories or submit pull / merge requests to existing @@ -45,7 +45,7 @@ add the `repoVisibility` key within a software template: repoVisibility: public # or 'internal' or 'private' ``` -### Disabling Docker in Docker situation (Optional) +## Disabling Docker in Docker situation (Optional) Software templates use the `fetch:template` action by default, which requires no external dependencies and offers a @@ -68,7 +68,7 @@ RUN apt-get update && apt-get install -y python3 python3-pip RUN pip3 install cookiecutter ``` -### Customizing the ScaffolderPage with Grouping and Filtering +## Customizing the ScaffolderPage with Grouping and Filtering Once you have more than a few software templates you may want to customize your `ScaffolderPage` by grouping and surfacing certain templates together. You can diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index a854543acb..5911e192b1 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -15,7 +15,7 @@ locations like GitHub or GitLab. -### Getting Started +## Getting Started > Be sure to have covered > [Getting Started with Backstage](../../getting-started) before proceeding. @@ -27,7 +27,7 @@ Once there, you should see something that looks similar to this: ![Create Image](../../assets/software-templates/create.png) -### Choose a template +## Choose a template When you select a template that you want to create, you'll be taken to the next page which may or may not look different for each template. Each template can @@ -44,7 +44,7 @@ provider, for instance `https://github.com/backstage/my-new-repository`, or ![Enter Backstage vars](../../assets/software-templates/template-picked-2.png) -### Run! +## Run! Once you've entered values and confirmed, you'll then get a popup box with live progress of what is currently happening with the creation of your template. @@ -60,7 +60,7 @@ step that failed which can be helpful in debugging. ![Templating failed](../../assets/software-templates/failed.png) -### View Component in Catalog +## View Component in Catalog When it's been created, you'll see the `View in Catalog` button, which will take you to the registered component in the catalog: diff --git a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md index 559cb036e0..1f771b4eaf 100644 --- a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md +++ b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md @@ -162,6 +162,24 @@ away in future versions and the `RepoUrlPicker` will return an object so `parameters.repoUrl` will already be a `{ host: string; owner: string; repo: string }` 🚀 +## Links should be used instead of named outputs + +Previously, it was possible to provide links to the frontend using the named output `entityRef` and `remoteUrl`. +These should be moved to `links` under the `output` object instead. + +```diff + output: +- remoteUrl: '{{ steps.publish.output.remoteUrl }}' +- entityRef: '{{ steps.register.output.entityRef }}' ++ links: ++ - title: Repository ++ url: ${{ steps.publish.output.remoteUrl }} ++ - title: Open in catalog ++ icon: catalog ++ entityRef: ${{ steps.register.output.entityRef }} + +``` + ### Summary Of course, we're always available on [discord](https://discord.gg/MUpMjP2) if diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 5afccc3450..8c57f202a7 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -12,7 +12,7 @@ by writing custom actions which can be used along side our > built-in actions too**. To ensure you can continue to include the builtin > actions, see below to include them during registration of your action. -### Writing your Custom Action +## Writing your Custom Action Your custom action can live where you choose, but simplest is to include it alongside your `backend` package in `packages/backend`. @@ -79,7 +79,7 @@ The `createTemplateAction` takes an object which specifies the following: function using `ctx.output` - `handler` - the actual code which is run part of the action, with a context -#### The context object +### The context object When the action `handler` is called, we provide you a `context` as the only argument. It looks like the following: @@ -98,7 +98,7 @@ argument. It looks like the following: - `ctx.metadata` - an object containing a `name` field, indicating the template name. More metadata fields may be added later. -### Registering Custom Actions +## Registering Custom Actions Once you have your Custom Action ready for usage with the scaffolder, you'll need to pass this into the `scaffolder-backend` `createRouter` function. You @@ -145,7 +145,7 @@ return await createRouter({ }); ``` -### List of custom action packages +## List of custom action packages Here is a list of Open Source custom actions that you can add to your Backstage scaffolder backend: diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index fab2e1e2cb..d5d4afbff2 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -141,7 +141,7 @@ Once it's been passed to the `ScaffolderPage` you should now be able to use the Something like this: ```yaml -apiVersion: backstage.io/v1beta2 +apiVersion: scaffolder.backstage.io/v1beta3 kind: Template metadata: name: Test template diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 415f8cd85e..41061d45a3 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -99,7 +99,7 @@ spec: Let's dive in and pick apart what each of these sections do and what they are. -### `spec.parameters` - `FormStep | FormStep[]` +## `spec.parameters` - `FormStep | FormStep[]` These `parameters` are template variables which can be modified in the frontend as a sequence. It can either be one `Step` if you just want one big list of @@ -227,7 +227,7 @@ spec: inputType: tel ``` -#### Hide or mask sensitive data on Review step +### Hide or mask sensitive data on Review step Sometimes, specially in custom fields, you collect some data on Create form that must not be shown to the user on Review step. To hide or mask this data, you can @@ -254,7 +254,7 @@ use `ui:widget: password` or set some properties of `ui:backstage`: show: false # wont print any info about 'hidden' property on Review Step ``` -#### The Repository Picker +### The Repository Picker In order to make working with repository providers easier, we've built a custom picker that can be used by overriding the `ui:field` option in the `uiSchema` @@ -287,7 +287,7 @@ The `RepoUrlPicker` is a custom field that we provide part of the `plugin-scaffolder`. You can provide your own custom fields by [writing your own Custom Field Extensions](./writing-custom-field-extensions.md) -##### Using the Users `oauth` token +#### Using the Users `oauth` token There's a little bit of extra magic that you get out of the box when using the `RepoUrlPicker` as a field input. You can provide some additional options under @@ -360,7 +360,7 @@ There's also the ability to pass additional scopes when requesting the `oauth` token from the user, which you can do on a per-provider basis, in case your template can be published to multiple providers. -#### The Owner Picker +### The Owner Picker When the scaffolder needs to add new components to the catalog, it needs to have an owner for them. Ideally, users should be able to select an owner when they go @@ -380,7 +380,7 @@ owner: - Group ``` -### `spec.steps` - `Action[]` +## `spec.steps` - `Action[]` The `steps` is an array of the things that you want to happen part of this template. These follow the same standard format: @@ -400,7 +400,7 @@ By default we ship some [built in actions](./builtin-actions.md) that you can take a look at, or you can [create your own custom actions](./writing-custom-actions.md). -### Outputs +## Outputs Each individual step can output some variables that can be used in the scaffolder frontend for after the job is finished. This is useful for things @@ -415,7 +415,7 @@ output: entityRef: ${{ steps.register.output.entityRef }} # link to the entity that has been ingested to the catalog ``` -### The templating syntax +## The templating syntax You might have noticed variables wrapped in `${{ }}` in the examples. These are template strings for linking and gluing the different parts of the template diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index a5dc8c3c54..6efb83549e 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -97,7 +97,7 @@ techdocs-cli generate Alias: `techdocs-cli build` The generate command uses the -[`@backstage/techdocs-common`](https://github.com/backstage/backstage/tree/master/packages/techdocs-common) +[`@backstage/plugin-techdocs-node`](https://github.com/backstage/backstage/tree/master/plugins/techdocs-node) package from Backstage for consistency. A Backstage app can also generate and publish TechDocs sites if `techdocs.builder` is set to `'local'` in `app-config.yaml`. See @@ -130,6 +130,8 @@ Options: if not found. --etag A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json. + --omitTechdocsCoreMkdocsPlugin An option to disable automatic addition of techdocs-core plugin to the mkdocs.yaml files. + Defaults to false, which means that the techdocs-core plugin is always added to the mkdocs file. -v --verbose Enable verbose output. (default: false) -h, --help display help for command ``` diff --git a/docs/features/techdocs/concepts.md b/docs/features/techdocs/concepts.md index 408f092ebd..e34f58bc70 100644 --- a/docs/features/techdocs/concepts.md +++ b/docs/features/techdocs/concepts.md @@ -46,6 +46,24 @@ between `techdocs-backend` and the storage) [TechDocs Backend](https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend) +## TechDocs Build Strategy + +To accommodate more complex logic surrounding whether or not to build TechDocs, the TechDocs backend +supports selecting a Build Strategy. +The Build Strategy is responsible for deciding whether the documentation requested should be built locally +by the TechDocs backend or not. +Customization of the Build Strategy allows for more complex behaviour regarding whether the TechDocs backend +is responsible for building TechDocs, whether an external process is responsible, or whether a combination +of local builds and an external process is responsible, on an entity-by-entity basis. + +The default Build Strategy results in the TechDocs backend building documentation locally if the +`techdocs.builder` configuration option is set to `'local'`, and skipping any building otherwise. +However any logic that satisfies the Build Strategy interface can be implemented, using the Backstage +config as well as the entity being processed to make a decision. + +For an example of how the Build Strategy can be used to implement a 'hybrid' build model, refer to +the [How to implement a hybrid build strategy](./how-to-guides.md#how-to-implement-a-hybrid-build-strategy) guide. + ## TechDocs Container The TechDocs container is a Docker container available at diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 6317ae365b..3edc55574f 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -37,12 +37,22 @@ techdocs: pullImage: true + mkdocs: + # (Optional) techdocs.generator.omitTechdocsCoreMkdocsPlugin can be used to disable automatic addition of techdocs-core plugin to the mkdocs.yaml files. + # Defaults to false, which means that the techdocs-core plugin is always added to the mkdocs file. + omitTechdocsCorePlugin: false + # techdocs.builder can be either 'local' or 'external. - # If builder is set to 'local' and you open a TechDocs page, techdocs-backend will try to generate the docs, publish to storage - # and show the generated docs afterwords. This is the "Basic" setup of the TechDocs Architecture. - # If builder is set to 'external', techdocs-backend will only fetch the docs and will NOT try to generate and publish. In this case of 'external', - # we assume that docs are being built by an external process (e.g. in the CI/CD pipeline of the repository). This is the "Recommended" setup of - # the architecture. Read more here https://backstage.io/docs/features/techdocs/architecture + # Using the default build strategy, if builder is set to 'local' and you open a TechDocs page, + # techdocs-backend will try to generate the docs, publish to storage and show the generated docs afterwords. + # This is the "Basic" setup of the TechDocs Architecture. + # Using the default build strategy, if builder is set to 'external' (or anything other than 'local'), techdocs-backend + # will only fetch the docs and will NOT try to generate and publish. + # In this case, we assume that docs are being built by an external process (e.g. in the CI/CD pipeline of the repository). + # This is the "Recommended" setup of the architecture. + # Note that custom build strategies may alter this behaviour. + # Read more about the "Basic" and "Recommended" setups here https://backstage.io/docs/features/techdocs/architecture + # Read more about build strategies here: https://backstage.io/docs/features/techdocs/concepts#techdocs-build-strategy builder: 'local' diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index 383c3ae6d2..f63ff43b7f 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -79,6 +79,9 @@ plugins: - techdocs-core ``` +> Note - The plugins section above is optional. Backstage automatically adds the `techdocs-core` plugin to the +> mkdocs file if it is missing. This functionality can be turned off with a [configuration option](./configuration.md) in Backstage. + Update your component's entity description by adding the following lines to its `catalog-info.yaml` in the root of its repository: diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 863b4b3ecb..56ca2252d0 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -538,3 +538,58 @@ Done! Now you have a support of the following diagrams along with mermaid: - `Vega` - `Vega-Lite` - `WaveDrom` + +## How to implement a hybrid build strategy + +One limitation of the [Recommended deployment](./architecture.md#recommended-deployment) is that +the experience for users requires modifying their CI/CD process to publish +their TechDocs. For some users, this may be unnecessary, and provides a barrier +to entry for onboarding users to Backstage. However, a purely local TechDocs +build restricts TechDocs creators to using the tooling provided in Backstage, +as well as the plugins and features provided in the Backstage-included `mkdocs` +installation. + +To accommodate both of these use-cases, users can implement a custom [Build Strategy](./concepts.md#techdocs-build-strategy) +with logic to encode which TechDocs should be built locally, and which will be +built externally. + +To achieve this hybrid build model: + +1. In your Backstage instance's `app-config.yaml`, set `techdocs.builder` to + `'local'`. This ensures that Backstage will build docs for users who want the + 'out-of-the-box' experience. +2. Configure external storage of TechDocs as normal for a production deployment. + This allows Backstage to publish documentation to your storage, as well as + allowing other users to publish documentation from their CI/CD pipelines. +3. Create a custom build strategy, that implements the `DocsBuildStrategy` interface, + and which implements your custom logic for determining whether to build docs for + a given entity. + For example, to only build docs when an entity has the `company.com/techdocs-builder` + annotation set to `'local'`: + + ```typescript + export class AnnotationBasedBuildStrategy { + private readonly config: Config; + + constructor(config: Config) { + this.config = config; + } + + async shouldBuild(_: Entity): Promise { + return ( + this.entity.metadata?.annotations?.['company.com/techdocs-builder'] === + 'local' + ); + } + } + ``` + +4. Pass an instance of this Build Strategy as the `docsBuildStrategy` parameter of the + TechDocs backend `createRouter` method. + +Users should now be able to choose to have their documentation built and published by +the TechDocs backend by adding the `company.com/techdocs-builder` annotation to their +entity. If the value of this annotation is `'local'`, the TechDocs backend will build +and publish the documentation for them. If the value of the `company.com/techdocs-builder` +annotation is anything other than `'local'`, the user is responsible for publishing +documentation to the appropriate location in the TechDocs external storage. diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index 34eca2429a..b661afcdb1 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -57,6 +57,7 @@ done like this: import { createApp } from '@backstage/app-defaults'; import { ThemeProvider } from '@material-ui/core/styles'; import CssBaseline from '@material-ui/core/CssBaseline'; +import LightIcon from '@material-ui/icons/WbSunny'; const app = createApp({ apis: ..., @@ -65,6 +66,7 @@ const app = createApp({ id: 'my-theme', title: 'My Custom Theme', variant: 'light', + icon: , Provider: ({ children }) => ( {children} diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 082fbf5b70..26dbca1442 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -133,9 +133,9 @@ familiar with. For other options, see Go to [https://github.com/settings/applications/new](https://github.com/settings/applications/new) to create your OAuth App. The `Homepage URL` should point to Backstage's -frontend, in our tutorial it would be `http://127.0.0.1:3000`. The +frontend, in our tutorial it would be `http://localhost:3000`. The `Authorization callback URL` will point to the auth backend, which will most -likely be `http://127.0.0.1:7007/api/auth/github/handler/frame`. +likely be `http://localhost:7007/api/auth/github/handler/frame`.

Screenshot of the GitHub OAuth creation page diff --git a/docs/getting-started/project-structure.md b/docs/getting-started/project-structure.md index 6c49c798d0..b67e847272 100644 --- a/docs/getting-started/project-structure.md +++ b/docs/getting-started/project-structure.md @@ -160,8 +160,8 @@ are separated out into their own folder, see further down. reusable React components. Stories are within the core package, and are published in the [Backstage Storybook](https://backstage.io/storybook). -- [`techdocs-common/`](https://github.com/backstage/backstage/tree/master/packages/techdocs-common) - - Common functionalities for TechDocs, to be shared between +- [`techdocs-node/`](https://github.com/backstage/backstage/tree/master/plugins/techdocs-node) - + Common node.js functionalities for TechDocs, to be shared between [techdocs-backend](https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend) plugin and [techdocs-cli](https://github.com/backstage/techdocs-cli). diff --git a/docs/integrations/aws-s3/discovery.md b/docs/integrations/aws-s3/discovery.md index 0ce660049a..5dfaf9dc39 100644 --- a/docs/integrations/aws-s3/discovery.md +++ b/docs/integrations/aws-s3/discovery.md @@ -39,7 +39,7 @@ Once you've done that, you'll also need to add the segment below to `packages/ba ```ts /* packages/backend/src/plugins/catalog.ts */ -import { AwsS3DiscoveryProcessor } from '@backstage/plugin-catalog-backend'; +import { AwsS3DiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-aws'; const builder = await CatalogBuilder.create(env); /** ... other processors ... */ diff --git a/docs/integrations/bitbucket/discovery.md b/docs/integrations/bitbucket/discovery.md index b08386f3c7..28b0128f56 100644 --- a/docs/integrations/bitbucket/discovery.md +++ b/docs/integrations/bitbucket/discovery.md @@ -80,6 +80,18 @@ The target is composed of the following parts: reduce the amount of API calls if you have a large workspace. [See here for the specification](https://developer.atlassian.com/bitbucket/api/2/reference/meta/filtering) for the query argument (will be passed as the `q` query parameter). +- (Optional) The `search=true` query argument to activate the mode utilizing code search. + - Is mutually exclusive to the `q` query argument. + - Allows providing values at `catalogPath` for finding catalog files as allowed by the `path` filter/modifier + [at Bitbucket Cloud's code search](https://confluence.atlassian.com/bitbucket/code-search-in-bitbucket-873876782.html#Search-Pathmodifier). + - `catalogPath=/catalog-info.yaml` + - `catalogPath=catalog-info.yaml` (anywhere in the repository) + - `catalogPath=/path/catalog-info.yaml` + - `catalogPath=path/catalog-info.yaml` + - `catalogPath=/path/*/catalog-info.yaml` + - `catalogPath=path/*/catalog-info.yaml` + - Supports multiple catalog files per repository depending on the `catalogPath` value. + - Registers `Location` entities for existing files only vs all matching repositories. Examples: @@ -95,6 +107,23 @@ Examples: - `https://bitbucket.org/workspaces/my-workspace?catalogPath=my/nested/path/catalog.yaml` will find all repositories in the `my-workspace` workspace and use the catalog file at `my/nested/path/catalog.yaml`. +- `https://bitbucket.org/workspaces/my-workspace?search=true&catalogPath=/catalog.yaml` + will find all `catalog.yaml` files located in the root of repositories in the workspace `my-workspace`. +- `https://bitbucket.org/workspaces/my-workspace?search=true&catalogPath=catalog.yaml` + will find all `catalog.yaml` files located anywhere within repositories in the workspace `my-workspace`. +- `https://bitbucket.org/workspaces/my-workspace?search=true&catalogPath=/my/nested/path/catalog.yaml` + will find all `catalog.yaml` files located within the directory `/my/nested/path/` within + repositories in the workspace `my-workspace`. +- `https://bitbucket.org/workspaces/my-workspace?search=true&catalogPath=my/nested/path/catalog.yaml` + will find all `catalog.yaml` files located within the directory `my/nested/path/` located anywhere within + repositories in the workspace `my-workspace`. +- `https://bitbucket.org/workspaces/my-workspace?search=true&catalogPath=/my/*/path/catalog.yaml` + will find all `catalog.yaml` files located within a directory `path/` located within any (recursive) directory + within the directory `my/` in the root of repositories in the workspace `my-workspace` + (`/my/nested/path/catalog.yaml`, `/my/very/nested/path/catalog.yaml`, ...). +- `https://bitbucket.org/workspaces/my-workspace/projects/apis-*/repos/service-*?search=true&catalogPath=catalog.yaml` + will find all `catalog.yaml` files located anywhere within repositories starting with `service-` + in projects starting with `api-` in the workspace `my-workspace`. ## Custom repository processing diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index ade575b74d..8d805fa362 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -34,3 +34,19 @@ The target is composed of three parts: a similar variation for catalog files stored in the root directory of each repository. If you want to use the repository's default branch use the `*` wildcard, e.g.: `/blob/*/catalog-info.yaml` + +Finally, you will have to add the processor in the catalog initialization code +of your backend. + +```diff +// In packages/backend/src/plugins/catalog.ts ++import { GitLabDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-gitlab'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); ++ builder.addProcessor( ++ GitLabDiscoveryProcessor.fromConfig(env.config, { logger: env.logger }) ++ ); +``` diff --git a/docs/integrations/ldap/org.md b/docs/integrations/ldap/org.md index 75e29687d7..f7f4e25b1a 100644 --- a/docs/integrations/ldap/org.md +++ b/docs/integrations/ldap/org.md @@ -14,9 +14,11 @@ entities that mirror your org setup. ## Installation -1. The processor is not installed by default, therefore you have to add a - dependency to `@backstage/plugin-catalog-backend-module-ldap` to your backend - package. +This guide will use the Entity Provider method. If you for some reason prefer +the Processor method (not recommended), it is described separately below. + +The provider is not installed by default, therefore you have to add a dependency +to `@backstage/plugin-catalog-backend-module-ldap` to your backend package. ```bash # From your Backstage root directory @@ -24,63 +26,73 @@ cd packages/backend yarn add @backstage/plugin-catalog-backend-module-ldap ``` -2. The `LdapOrgReaderProcessor` is not registered by default, so you have to - register it in the catalog plugin: +> Note: When configuring to use a Provider instead of a Processor you do not +> need to add a _location_ pointing to your LDAP server -```typescript -// packages/backend/src/plugins/catalog.ts -builder.addProcessor( - LdapOrgReaderProcessor.fromConfig(env.config, { - logger: env.logger, - }), -); +Update the catalog plugin initialization in your backend to add the provider and +schedule it: + +```diff + // packages/backend/src/plugins/catalog.ts ++import { Duration } from 'luxon'; ++import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); + ++ // The target parameter below needs to match the ldap.providers.target ++ // value specified in your app-config. ++ builder.addEntityProvider( ++ LdapOrgEntityProvider.fromConfig(env.config, { ++ id: 'our-ldap-master', ++ target: 'ldaps://ds.example.net', ++ logger: env.logger, ++ schedule: env.scheduler.createScheduledTaskRunner({ ++ frequency: Duration.fromObject({ minutes: 60 }), ++ timeout: Duration.fromObject({ minutes: 15 }), ++ }), ++ }), ++ ); ``` +After this, you also have to add some configuration in your app-config that +describes what you want to import for that target. + ## Configuration The following configuration is a small example of how a setup could look for importing groups and users from a corporate LDAP server. ```yaml -catalog: - locations: - - type: ldap-org - target: ldaps://ds.example.net - processors: - ldapOrg: - providers: - - target: ldaps://ds.example.net - bind: - dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net - secret: ${LDAP_SECRET} - users: - dn: ou=people,ou=example,dc=example,dc=net - options: - filter: (uid=*) - map: - description: l - set: - metadata.customField: 'hello' - groups: - dn: ou=access,ou=groups,ou=example,dc=example,dc=net - options: - filter: (&(objectClass=some-group-class)(!(groupType=email))) - map: - description: l - set: - metadata.customField: 'hello' +ldap: + providers: + - target: ldaps://ds.example.net + bind: + dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net + secret: ${LDAP_SECRET} + users: + dn: ou=people,ou=example,dc=example,dc=net + options: + filter: (uid=*) + map: + description: l + set: + metadata.customField: 'hello' + groups: + dn: ou=access,ou=groups,ou=example,dc=example,dc=net + options: + filter: (&(objectClass=some-group-class)(!(groupType=email))) + map: + description: l + set: + metadata.customField: 'hello' ``` -Locations point out the specific org(s) you want to import. The `type` of these -locations must be `ldap-org`, and the `target` must point to the exact URL -(starting with `ldap://` or `ldaps://`) of the targeted LDAP server. You can -have several such location entries if you want, but typically you will have just -one. - -The processor itself is configured in the other block, under -`catalog.processors.ldapOrg`. There may be many providers, each targeting a -specific `target` which is supposed to be on the same form as the location -`target`. +There may be many providers, each targeting a specific `target` which is +supposed to match the `target` of a dedicated provider instance - i.e., you will +add one entity provider class instance per target to ingest from. These config blocks have a lot of options in them, so we will describe each "root" key within the block separately. @@ -163,7 +175,7 @@ below, with their default values, but they are all optional. If you leave out an optional mapping, it will still be copied using that default value. For example, even if you do not put in the field `displayName` in your -config, the processor will still copy the attribute `cn` into the entity field +config, the provider will still copy the attribute `cn` into the entity field `spec.profile.displayName`. ```yaml @@ -245,7 +257,7 @@ shown below, with their default values, but they are all optional. If you leave out an optional mapping, it will still be copied using that default value. For example, even if you do not put in the field `displayName` in your -config, the processor will still copy the attribute `cn` into the entity field +config, the provider will still copy the attribute `cn` into the entity field `spec.profile.displayName`. If the target field is optional, such as the display name, the importer will accept missing attributes and just leave the target field unset. If the target field is mandatory, such as the name of the entity, @@ -283,94 +295,74 @@ map: members: member ``` -## Customize the Processor +## Customize the Provider -In case you want to customize the ingested entities, the -`LdapOrgReaderProcessor` allows to pass transformers for users and groups. +In case you want to customize the ingested entities, the provider allows to pass +transformers for users and groups. Here we will show an example of overriding +the group transformer. 1. Create a transformer: -```ts -export async function myGroupTransformer( - vendor: LdapVendor, - config: GroupConfig, - group: SearchEntry, -): Promise { - // Transformations may change namespace, change entity naming pattern, fill - // profile with more or other details... + ```ts + export async function myGroupTransformer( + vendor: LdapVendor, + config: GroupConfig, + group: SearchEntry, + ): Promise { + // Transformations may change namespace, change entity naming pattern, fill + // profile with more or other details... - // Create the group entity on your own, or wrap the default transformer - return await defaultGroupTransformer(vendor, config, group); -} -``` + // Create the group entity on your own, or wrap the default transformer + return await defaultGroupTransformer(vendor, config, group); + } + ``` -2. Configure the processor with the transformer: +2. Configure the provider with the transformer: -```ts + ```ts + const ldapEntityProvider = LdapOrgEntityProvider.fromConfig(env.config, { + id: 'our-ldap-master', + target: 'ldaps://ds.example.net', + logger: env.logger, + groupTransformer: myGroupTransformer, + }); + ``` + +## Using a Processor instead of a Provider + +An alternative to using the Provider for ingesting LDAP entries is to use a +Processor. This is the old way that's based on registering locations with the +proper type and target, triggering the processor to run. + +The drawback of this method is that it will leave orphaned Group/User entities +whenever they are deleted on your LDAP server, and you cannot control the +frequency with which they are refreshed, separately from other processors. + +### Processor Installation + +The `LdapOrgReaderProcessor` is not registered by default, so you have to +register it in the catalog plugin: + +```typescript +// packages/backend/src/plugins/catalog.ts builder.addProcessor( - LdapOrgReaderProcessor.fromConfig(config, { - logger, - groupTransformer: myGroupTransformer, + LdapOrgReaderProcessor.fromConfig(env.config, { + logger: env.logger, }), ); ``` -## Using a Provider instead of a Processor +### Driving LDAP Org Processor Ingestion with Locations -An alternative to using the Processor for ingesting LDAP entries is to use a -Provider. Doing this can give you a little bit more freedom to handle the LDAP -ingestion more independently from the rest of the catalog ingestion. +Locations point out the specific org(s) you want to import. The `type` of these +locations must be `ldap-org`, and the `target` must point to the exact URL +(starting with `ldap://` or `ldaps://`) of the targeted LDAP server. You can +have several such location entries if you want, but typically you will have just +one. -This can be useful if you have a lot of Users and Groups and hitting your LDAP -server is resource intensive but you still want your other catalog entries to be -updated frequently. - -> Note: When configuring to use a Provider instead of a Processor you do not -> need to add a _location_ pointing to your LDAP server - -```ts -// packages/backend/src/plugins/catalog.ts -import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -import { PluginEnvironment } from '../types'; - -import { Router } from 'express'; -import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap'; - -import { Duration } from 'luxon'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const ldapEntityProvider = LdapOrgEntityProvider.fromConfig(env.config, { - id: 'custom-ldap', - // target needs to match the catalog.processors.ldapOrg.providers.target specified in app-config - target: 'ldaps://ds.example.net', - logger: env.logger, - }); - - const builder = await CatalogBuilder.create(env); - builder.addEntityProvider(ldapEntityProvider); - - // You can change the refresh interval for the other catalog entries independently, or just leave the line below out to use the default refresh interval - builder.setRefreshIntervalSeconds(100); - - const { processingEngine, router } = await builder.build(); - await processingEngine.start(); - - await env.scheduler.scheduleTask({ - id: 'refresh_ldap', - // frequency sets how often you want to ingest users and groups from LDAP, in this case every 60 minutes - frequency: Duration.fromObject({ minutes: 60 }), - timeout: Duration.fromObject({ minutes: 15 }), - fn: async () => { - try { - await ldapEntityProvider.read(); - } catch (error) { - env.logger.error(error); - } - }, - }); - - return router; -} +```yaml +catalog: + locations: + - type: ldap-org + target: ldaps://ds.example.net ``` diff --git a/docs/local-dev/cli-build-system.md b/docs/local-dev/cli-build-system.md index 75f0606752..be07a67f22 100644 --- a/docs/local-dev/cli-build-system.md +++ b/docs/local-dev/cli-build-system.md @@ -72,6 +72,53 @@ or IDE that has support for formatting, linting, and type checking. Let's dive into a detailed look at each of these steps and how they are implemented in a typical Backstage app. +## Package Roles + +> Package roles were introduced in March 2022. To migrate existing projects, see the [migration guide](../tutorials/package-role-migration.md). + +The Backstage build system uses the concept of package roles in order to help keep +configuration lean, provide utility and tooling, and enable optimizations. A package +role is a single string that identifies what the purpose of a package is, and it's +defined in the `package.json` of each package like this: + +```json +{ + "name": "my-package", + "backstage": { + "role": "" + }, + ... +} +``` + +These are the available roles that are currently supported by the Backstage build system: + +| Role | Description | Example | +| ---------------------- | -------------------------------------------- | -------------------------------------------- | +| frontend | Bundled frontend application | `package/app` | +| backend | Bundled backend application | `packages/backend` | +| cli | Package used as a command-line interface | `@backstage/cli`, `@backstage/codemods` | +| web-library | Web library for use by other packages | `@backstage/plugin-catalog-react` | +| node-library | Node.js library for use by other packages | `@backstage/plugin-techdocs-node` | +| common-library | Isomorphic library for use by other packages | `@backstage/plugin-permission-common` | +| frontend-plugin | Backstage frontend plugin | `@backstage/plugin-scaffolder` | +| frontend-plugin-module | Backstage frontend plugin module | `@backstage/plugin-analytics-module-ga` | +| backend-plugin | Backstage backend plugin | `@backstage/plugin-auth-backend` | +| backend-plugin-module | Backstage backend plugin module | `@backstage/plugin-search-backend-module-pg` | + +Most of the steps that we cover below have an accompanying command that is intended to be used as a package script. The commands are all available under the `backstage-cli package` category, and many of the commands will behave differently depending on the role of the package. The commands are intended to be used like this: + +```json +{ + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + ... + } +} +``` + ## Formatting The formatting setup lives completely within each Backstage application and is @@ -96,9 +143,38 @@ configurations in turn build on top of the lint rules from In a standard Backstage setup, each individual package has its own lint configuration, along with a root configuration that applies to the entire -project. Each configuration is initially one that simply extends a base -configuration provided by the Backstage CLI, but they can be customized to fit -the needs of each package. +project. The configuration in each package starts out as a standard configuration +that is determined based on the package role, but it can be customized to fit the needs of each package. + +A minimal `.eslintrc.js` configuration now looks like this: + +```js +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +``` + +But you can provide custom overrides for each package using the optional second argument: + +```js +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + ignorePatterns: ['templates/'], + rules: { + 'jest/expect-expect': 'off', + }, +}); +``` + +The configuration factory also provides utilities for extending the configuration in ways that are otherwise very cumbersome to do with plain ESLint, particularly for rules like `no-restricted-syntax`. These are the extra keys that are available: + +| Key | Description | +| ----------------------- | ------------------------------------------------------------------ | +| `tsRules` | Additional rules to apply to TypeScript files | +| `testRules` | Additional rules to apply to tests files | +| `restrictedImports` | Additional paths to add to `no-restricted-imports` | +| `restrictedSrcImports` | Additional paths to add to `no-restricted-imports` in src files | +| `restrictedTestImports` | Additional paths to add to `no-restricted-imports` in test files | +| `restrictedSyntax` | Additional patterns to add to `no-restricted-syntax` | +| `restrictedSrcSyntax` | Additional patterns to add to `no-restricted-syntax` in src files | +| `restrictedTestSyntax` | Additional patterns to add to `no-restricted-syntax` in test files | ## Type Checking @@ -167,11 +243,8 @@ nevertheless be useful to know how it works, since all of the published Backstage packages are built using this process. The build is currently using [Rollup](https://rollupjs.org/) and executes in -isolation for each individual package. There are currently three different -commands in the Backstage CLI that invokes the build process, `plugin:build`, -`backend:build`, and simply `build`. The two former are pre-configured commands -for frontend and backend plugins, while the `build` command provides more -control over the output. +isolation for each individual package. The build is invoked using the `package build` +command, and applies to all packages roles except the bundled ones, `frontend` and `backend`. There are three different possible outputs of the build process: JavaScript in CommonJS module format, JavaScript in ECMAScript module format, and type @@ -181,12 +254,10 @@ files like stylesheets or images. For more details on what syntax and file formats are supported by the build process, see the [loaders section](#loaders). When building CommonJS or ESM output, the build commands will always use -`src/index.ts` as the entrypoint. All dependencies of the package will be marked -as external, meaning that in general it is only the contents of the `src` folder -that ends up being compiled and output to `dist`. All import statements of -external dependencies, even within the same monorepo, will stay intact. The -externalized dependencies are based on dependency information in `package.json`, -which means it's important to keep it up to date. +`src/index.ts` as the entrypoint. All non-relative modules imports are considered +external, meaning the Rollup build will only compile the source code of the package +itself. All import statements of external dependencies, even within the same +monorepo, will stay intact. The build of the type definitions works quite differently. The entrypoint of the type definition build is the relative location of the package within the @@ -207,11 +278,11 @@ cover each combination of these cases separately. ### Frontend Development -There are two different commands that start the frontend development bundling: -`app:serve`, which serves an app and uses `src/index` as the entrypoint, and -`plugin:serve`, which serves a plugin and uses `dev/index` as the entrypoint. -These are typically invoked via the `yarn start` script, and are intended for -local development only. When running the bundle command, a development server +The frontend development setup is used for all packages with a frontend role, and +is invoked using the `package start` command. +The only difference between the different roles is that packages with the `'frontend'` +role use `src/index` as the entrypoint, while other roles instead use `dev/index`. +When running the start command, a development server will be set up that listens to the protocol, host and port set by `app.baseUrl` in the configuration. If needed it is also possible to override the listening options through the `app.listen` configuration. @@ -235,8 +306,8 @@ support for them instead. ### Frontend Production The frontend production bundling creates your typical web content bundle, all -contained within a single folder, ready for static serving. It is invoked using -the `app:build` command, and unlike the development bundling there is no way to +contained within a single folder, ready for static serving. It is used when building +packages with the `'frontend'` role, and unlike the development bundling there is no way to build a production bundle of an individual plugin. The output of the bundling process is written to the `dist` folder in the package. @@ -255,13 +326,25 @@ correctly from linked in packages, the `ModuleScopePlugin` from [`react-dev-utils`](https://www.npmjs.com/package/react-dev-utils) which makes sure that imports don't reach outside the package, a few fallbacks for some Node.js modules like `'buffer'` and `'events'`, a plugin that writes the -frontend configuration to the bundle as `process.env.APP_CONFIG` and build -information as `process.env.BUILD_INFO`, and lastly minification handled by +frontend configuration to the bundle as `process.env.APP_CONFIG`, and lastly minification handled by [esbuild](https://esbuild.github.io/) using the [`esbuild-loader`](https://npm.im/esbuild-loader). There are of course also a set of loaders configured, which you can read more about in the [loaders](#loaders) and [transpilation](#transpilation) sections. +During the build, the following constants are also set: + +```java +process.env.NODE_ENV = 'production'; +process.env.BUILD_INFO = { + cliVersion: '0.4.0', // The version of the CLI package + gitVersion: 'v0.4.0-86-ge54815618', // output of `git describe --always` + packageVersion: '1.0.5', // The version of the app package itself + timestamp: 1678900000000, // Date.now() when the build started + commit: 'e548156182a973ed4b459e18533afc22c85ffff8', // output of `git rev-parse HEAD` +}; +``` + The output of the bundling process is split into two categories of files with separate caching strategies. The first is a set of generic assets with plain names in the root of the `dist/` folder. You will want to serve these with @@ -331,7 +414,7 @@ dependencies installed, and as soon as you copy over and extract the contents of the `bundle.tar.gz` archive on top of it, the backend will be ready to run. The following is an example of a `Dockerfile` that can be used to package the -output of `backstage-cli backend:bundle` into an image: +output of building a package with role `'backend'` into an image: ```Dockerfile FROM node:16-bullseye-slim @@ -543,12 +626,45 @@ The following is an excerpt of a typical setup of an isomorphic library package: "types": "dist/index.d.ts" }, "scripts": { - "build": "backstage-cli build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" }, "files": ["dist"], ``` + +## Experimental Type Build + +The Backstage CLI has an experimental feature where multiple different type definition files can be generated for different release stages. The release stages are marked in the [TSDoc](https://tsdoc.org/) for each individual export, using either `@public`, `@alpha`, or `@beta`. Rather than just building a single `index.d.ts` file, the build process will instead output `index.d.ts`, `index.beta.d.ts`, and `index.alpha.d.ts`. Each of these files will have exports from more unstable release stages stripped, meaning that `index.d.ts` will omit all exports marked with `@alpha` or `@beta`, while `index.beta.d.ts` will omit all exports marked with `@alpha`. + +This feature is aimed at projects that publish to package registries and wish to maintain different levels of API stability within each package. There is no need to use this within a single monorepo, as it has no effect due to only applying to built and published packages. + +In order for the experimental type build to work, `@microsoft/api-extractor` must be installed in your project, as it is an optional peer dependency of the Backstage CLI. There are then three steps that need to be taken for each package where you want to enable this feature: + +- Add the `--experimental-type-build` flag to the `"build"` script of the package. +- Add either one or both of `"alphaTypes"` and `"betaTypes"` to the `"publishConfig"` of the package: + ```json + "publishConfig": { + ... + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts", + "betaTypes": "dist/index.beta.d.ts" + }, + ``` +- Add either one or both of `"alpha"` and `"beta"` to the `"files"` of the package: + ```json + "files": [ + "dist", + "alpha", + "beta" + ] + ``` + +Once this setup is complete, users of the published packages will only be able to access the stable API via the main package entry point, for example `@acme/my-plugin`. Exports marked with `@alpha` or `@beta` will only be available via the `/alpha` entry point, for example `@acme/my-plugin/alpha`, and exports marked with `@beta` will only be available via `/beta`. This does not apply within the monorepo that contains the package. There all exports still have to be imported via the main entry point. + +Note that these different entry points are only separated during type checking. At runtime they all share the same code which contains the exports from all releases stages. + +An example of this setup can be seen in the [`@backstage/catalog-model`](https://github.com/backstage/backstage/blob/da0675bf9f28ed1460f03635a22d3c26abd14707/packages/catalog-model/package.json#L14) package, which has enabled `alpha` type exports. With this setup, exports marked as `@alpha` are only available for import via `@backstage/catalog-model/alpha`. The `@backstage/catalog-model` package currently does not have any exports marked as `@beta`, or a `/beta` entry point. diff --git a/docs/local-dev/cli-commands.md b/docs/local-dev/cli-commands.md index eff6404e3b..7157fad2bf 100644 --- a/docs/local-dev/cli-commands.md +++ b/docs/local-dev/cli-commands.md @@ -7,251 +7,202 @@ description: Descriptions of all commands available in the CLI. This page lists all commands provided by the Backstage CLI, what they're for, and where to use them. -The documentation for each command begins with specifying its scope, this -indicates where the command should be used by selecting from the following list: - -- `app` - A frontend app package, such as `packages/app`. -- `backend` - A backend package, such as `packages/backend`. -- `frontend-plugin` - A frontend plugin package. -- `backend-plugin` - A backend plugin package. -- `root` - The monorepo root. -- `any` - Any kind of package, but not the repo root. - ## help This command displays a help summary or detailed help screens for each command. -Below is a cleaned up output of `yarn backstage-cli --help`. +Below is a cleaned up output of `yarn backstage-cli --help` ```text -app:build Build an app for a production release -app:serve Serve an app for local development +repo [command] Command that run across an entire Backstage project +package [command] Lifecycle scripts for individual packages +migrate [command] Migration utilities -backend:build Build a backend plugin -backend:bundle Bundle the backend into a deployment archive -backend:build-image Bundles the package into a docker image -backend:dev Start local development server with HMR for the backend +create Open up an interactive guide to creating new things in your app -plugin:build Build a plugin -plugin:diff Diff an existing plugin with the creation template -plugin:serve Serves the dev/ folder of a plugin +config:docs Browse the configuration reference documentation +config:print Print the app configuration for the current package +config:check Validate that the given configuration loads and matches schema +config:schema Dump the app configuration schema -build Build a package for publishing -build-workspace Builds a temporary dist workspace from the provided packages -lint Lint a package -test Run tests, forwarding args to Jest, defaulting to watch mode -clean Delete cache directories +versions:bump Bump Backstage packages to the latest versions +versions:check Check Backstage package versioning -create Open up an interactive guide to creating new things in your app -create-plugin Creates a new plugin in the current repository -remove-plugin Removes plugin in the current repository +build-workspace Builds a temporary dist workspace from the provided packages +create-github-app Create new GitHub App in your organization (experimental) -config:docs Browse the configuration reference documentation -config:print Print the app configuration for the current package -config:check Validate that the given configuration loads and matches schema -config:schema Dump the app configuration schema - -versions:bump Bump Backstage packages to the latest versions -versions:check Check Backstage package versioning - -prepack Prepares a package for packaging before publishing -postpack Restores the changes made by the prepack command - -create-github-app Create new GitHub App in your organization (experimental) - -info Show helpful information for debugging and reporting bugs -help [command] display help for command +info Show helpful information for debugging and reporting bugs +help [command] display help for command ``` -## app:build - -Scope: `app` - -Builds a bundle of static content from the app, which can then be served via any -static web server such as `nginx`, or via the -[`app-backend`](https://www.npmjs.com/package/@backstage/plugin-app-backend) -plugin directly from a Backstage backend instance. - -The command also reads and injects static configuration into the bundle. It is -important to note that when deploying using your own static content hosting -solution, this will be the final configuration used in the frontend unless you -for example hook in configuration loading from the backend. When using the -`nginx` based Dockerfile in this repo along with its included run script, -`APP_CONFIG_` environment variables will be injected into the frontend, and when -serving using the `app-backend` plugin, the configuration is completely injected -from the backend and the configuration at the time of calling this command will -not be used. - -Note that even when injecting configuration at runtime, it is not possible to -change the base path of the app. For example, if you at build time have -`app.baseUrl` set to `http://dev-app.com/my-app`, you can change that to -`https://prod-app.com/my-app`, but not to `https://prod-app.com`, as that would -change the path. - -During the build, the following variables are set: - -```java -process.env.NODE_ENV = 'production'; -process.env.BUILD_INFO = { - cliVersion: '0.4.0', // The version of the CLI package - gitVersion: 'v0.4.0-86-ge54815618', // output of `git describe --always` - packageVersion: '1.0.5', // The version of the app package itself - timestamp: 1678900000000, // Date.now() when the build started - commit: 'e548156182a973ed4b459e18533afc22c85ffff8', // output of `git rev-parse HEAD` -}; -``` - -Some CI environments do not properly report correct resource limits, potentially -leading to errors such as `ENOMEM` during compilation. If you run into this -issue you can limit the parallelization of the build process by setting the -environment variable `BACKSTAGE_CLI_BUILD_PARALLEL`, which is forwarded to the -[`terser-webpack-plugin`](https://github.com/webpack-contrib/terser-webpack-plugin#parallel). -You can set it to `false` or `1` to completely disable parallelization, but -usually a low value such as `2` is enough. +The `package` command category, `yarn backstage-cli package --help` ```text -Usage: backstage-cli app:build +start [options] Start a package for local development +build [options] Build a package for production deployment or publishing +lint [options] Lint a package +test Run tests, forwarding args to Jest, defaulting to watch mode +clean Delete cache directories +prepack Prepares a package for packaging before publishing +postpack Restores the changes made by the prepack command +``` + +The `repo` command category, `yarn backstage-cli repo --help` + +```text +build [options] Build packages in the project, excluding bundled app and backend packages. +lint [options] Lint all packages in the project +``` + +The `migrate` command category, `yarn backstage-cli migrate --help` + +```text +package-roles Add package role field to packages that don't have it +package-scripts Set package scripts according to each package role +package-lint-configs Migrates all packages to use @backstage/cli/config/eslint-factory +``` + +## repo build + +Builds all packages in the project, excluding bundled packages by default, i.e. ones +with the role `'frontend'` or `'backend'`. + +```text +Usage: backstage-cli repo build [options] + +Build packages in the project, excluding bundled app and backend packages. + +Options: + --all Build all packages, including bundled app and backend packages. + --since <ref> Only build packages and their dev dependents that changed since the specified ref +``` + +## repo lint + +Lint all packages in the project. + +```text +Usage: backstage-cli repo lint [options] + +Lint all packages in the project + +Options: + --format <format> Lint report output format (default: "eslint-formatter-friendly") + --since <ref> Only lint packages that changed since the specified ref + --fix Attempt to automatically fix violations +``` + +## package start + +Starts the package for local development. See the frontend and backend development parts in the build system [bundling](./cli-build-system.md#bundling) section for more details. + +```text +Usage: backstage-cli package start [options] + +Start a package for local development Options: - --stats Write bundle stats to output directory - --lax Do not require environment variables to be set --config <path> Config files to load instead of app-config.yaml (default: []) - -h, --help display help for command + --role <name> Run the command with an explicit package role + --check Enable type checking and linting if available + --inspect Enable debugger in Node.js environments + --inspect-brk Enable debugger in Node.js environments, breaking before code starts ``` -## app:serve +## package build -Scope: `app` - -Serve an app for local development. This starts up a local development server, -using a bundling configuration that is quite similar to that of the `app:build` -command, but with development features such as React Hot Module Replacement, -faster sourcemaps, no minification, etc. - -The static configuration is injected into the frontend, but it does not support -watching, meaning that changes in for example `app-config.yaml` are not -reflected until the serve process is restarted. - -During the build, the following variables are set: - -```java -process.env.NODE_ENV = 'development'; -process.env.BUILD_INFO = { /* See app:build */ }; -``` - -The server listening configuration is controlled through the static -configuration. The `app.baseUrl` determines the listening host and port, as well -as whether HTTPS is used or not. It is also possible to override the listening -host and port if needed by setting `app.listen.host` and `app.listen.port`. +Build an individual package based on its role. See the build system [building](./cli-build-system.md#building) and [bundling](./cli-build-system.md#bundling) sections for more details. ```text -Usage: backstage-cli app:serve [options] +Usage: backstage-cli package build [options] + +Build a package for production deployment or publishing Options: - --check Enable type checking and linting - --config <path> Config files to load instead of app-config.yaml (default: []) - -h, --help display help for command + --role <name> Run the command with an explicit package role + --minify Minify the generated code. Does not apply to app or backend packages. + --experimental-type-build Enable experimental type build. Does not apply to app or backend packages. + --skip-build-dependencies Skip the automatic building of local dependencies. Applies to backend packages only. + --stats If bundle stats are available, write them to the output directory. Applies to app packages only. + --config <path> Config files to load instead of app-config.yaml. Applies to app packages only. (default: []) ``` -## backend:build +## package lint -Scope: `backend-plugin` - -This builds a backend package for publishing and use in production. The build -output is written to `dist/`. Be sure to list any additional file that the -package depends on at runtime in the `"files"` field inside `package.json`, a -common example being the `migrations` directory. +Lint a package. In addition to the default `eslint` behavior, this command will +include TypeScript files, treat warnings as errors, and default to linting the +entire directory if no specific files are listed. For more information, see the +build system [linting](./cli-build-system.md#linting) section. ```text -Usage: backstage-cli backend:build [options] +Usage: backstage-cli package lint [options] + +Lint a package Options: - --minify Minify the generated code - -h, --help display help for command + --format <format> Lint report output format (default: "eslint-formatter-friendly") + --fix Attempt to automatically fix violations ``` -## backend:bundle +## package test -Scope: `backend` +Run tests, forwarding all unknown options to Jest, and defaulting to watch mode. +When executing the tests, `process.env.NODE_ENV` will be set to `"test"`. -Bundles the backend into a `dist/bundle.tar.gz` archive. See the -[backend bundling](./cli-build-system.md#backend-production-bundling) build -systems documentation for more details. +This command uses a default Jest configuration that is included in the CLI, +which is set up with similar goals for speed, scale, and working within a +monorepo. The configuration sets the `src` as the root directory, enforces the +`.test.` infix for tests, and uses `src/setupTests.ts` as the test setup +location. The included configuration also supports test execution at the root of +a yarn workspaces monorepo by automatically creating one grouped configuration +that includes all packages that have `backstage-cli test` in their package +`test` script. + +For more information about configuration overrides and editor support, see the [Jest Configuration section](./cli-build-system.md#jest-configuration) in the build system documentation. ```text -Usage: backstage-cli backend:bundle [options] +Usage: backstage-cli package test [options] -Bundle the backend into a deployment archive +Run tests, forwarding args to Jest, defaulting to watch mode Options: - --build-dependencies Build all local package dependencies before bundling the backend - -h, --help display help for command -``` - -## backend:build-image - -Scope: `backend` - -Builds a Docker image of the backend package, forwarding all unknown options to -`docker image build`. For example: - -```bash -yarn backstage-cli backend:build-image --build --tag my-backend-image -``` - -The image is built using the backend package along with all of its local package -dependencies. It expects to find a `Dockerfile` at the root of the backend -package, which will be used during the build. - -The Dockerfile is **NOT** executed within the package or repo itself. Because -the packages in the repo itself are configured for development instead of -production use, the final Docker build happens in a separate temporary -directory, to which the backend package and dependencies have been copied. Only -files listed within the `"files"` field within each package's `package.json` are -copied over, along with the root `package.json`, `yarn.lock`, and any -`app-config.*.yaml` files. - -During the build a `skeleton.tar` file is created and put at the repo root. This -file contains the `package.json` of each included package, which together with -the root `package.json` and `yarn.lock` can be used to run a cached -`yarn install` before the full production builds of all the packages are copied -over, providing a significant speedup if Docker build layer caching available. - -This command is experimental and we hope to be able to replace it with one that -is less integrated directly with Docker, and also supports multi-stage Docker -builds. It is possible to replicate most of what this command does by manually -building each package, and then use the `build-workspace` to create the -temporary workspace, and finally copy over any additional files to the workspace -and execute the Docker build within it. - -```text -Usage: backstage-cli backend:build-image [options] - -Options: - --build Build packages before packing them into the image --backstage-cli-help display help for command ``` -## backend:dev +## package clean -Scope: `backend`, `backend-plugin` - -Starts a backend package in development mode, with watch mode enabled for all -local dependencies. +Remove cache and output directories. ```text -Usage: backstage-cli backend:dev [options] +Usage: backstage-cli package clean [options] -Options: - --check Enable type checking and linting - --inspect Enable debugger - --config <path> Config files to load instead of app-config.yaml (default: []) - -h, --help display help for command +Delete cache directories +``` + +## package prepack + +This command should be added as `scripts.prepack` in all packages. It enables +packaging- and publish-time overrides for fields inside `packages.json`. +For more details, see the build system [publishing](./cli-build-system.md#publishing) section. + +```text +Usage: backstage-cli package prepack [options] + +Prepares a package for packaging before publishing +``` + +## package postpack + +This should be added as `scripts.postpack` in all packages. It restores +`package.json` to what it looked like before calling the `prepack` command. + +```text +Usage: backstage-cli package postpack [options] + +Restores the changes made by the prepack command ``` ## create -Scope: `root` - The `create` command opens up an interactive guide for you to create new things in your app. If you do not pass in any options it is completely interactive, but it is possible to pre-select what you want to create using the `--select` flag, @@ -278,181 +229,16 @@ this: Usage: backstage-cli create [options] Options: - --select Select the thing you want to be creating upfront - --option = Pre-fill options for the creation process (default: []) - --scope The scope to use for new packages - --npm-registry The package registry to use for new packages + --select <name> Select the thing you want to be creating upfront + --option <name>=<value> Pre-fill options for the creation process (default: []) + --scope <scope> The scope to use for new packages + --npm-registry <URL> The package registry to use for new packages --no-private Do not mark new packages as private -h, --help display help for command ``` -## create-plugin - -Scope: `root` - -Creates a new plugin within the repository. This command is typically wrapped up -in the root `package.json` to be executed with `yarn create-plugin`, using -options that are appropriate for the organization that owns the app repo. A -recommended scope for internal packages is `@internal`. - -```text -Usage: backstage-cli create-plugin [options] - -Options: - --backend Create plugin with the backend dependencies as default - --scope <scope> npm scope - --npm-registry <URL> npm registry URL - --no-private Public npm package - -h, --help display help for command -``` - -## remove-plugin - -Scope: `root` - -A utility to remove a plugin from a repo, essentially undoing everything that -was done by `create-plugin`. - -This is primarily intended as a utility for manual tests and end to end testing -scripts. - -```text -Usage: backstage-cli remove-plugin [options] - -Options: - -h, --help display help for command -``` - -## plugin:build - -Scope: `frontend-plugin` - -Build a frontend plugin for publishing to a package registry. There is no need -to run this command during development or even in CI unless the package is being -published. The `app:bundle` command does not use the output for this command -when bundling local package dependencies. - -The output is written to a `dist/` folder. It also outputs type declarations for -the plugin, and therefore requires `yarn tsc` to have been run first. The input -type declarations are expected to be found within `dist-types/` at the root of -the monorepo. - -```text -Usage: backstage-cli plugin:build [options] - -Options: - --minify Minify the generated code - -h, --help display help for command -``` - -## plugin:serve - -Scope: `frontend-plugin` - -Serves a frontend plugin by itself for isolated development. The serve task -itself is essentially identical to `app:serve`, but the entrypoint is instead -set to the `dev/` folder within the plugin. - -The `dev/` folder typically contains a small wrapper script that hooks up any -necessary mock APIs or other things that are needed for the plugin to function. -The `@backstage/dev-utils` package provides utilities to that end. - -```text -Usage: backstage-cli plugin:serve [options] - -Options: - --check Enable type checking and linting - --config <path> Config files to load instead of app-config.yaml (default: []) - -h, --help display help for command -``` - -## plugin:diff - -Scope: `frontend-plugin` - -Compares a frontend plugin to the `create-plugin` template, making sure that it -hasn't diverged from the template and recommending updates when it has. A good -practice is to run this command after updating the version of the CLI in a -project. - -```text -Usage: backstage-cli plugin:diff [options] - -Options: - --check Fail if changes are required - --yes Apply all changes - -h, --help display help for command -``` - -## build - -Scope: `any` - -Build a single package for publishing, just like the `plugin:build` and -`backend:build` commands. This command is intended for standalone packages that -aren't plugins, and for example support building of isomorphic packages for -usage in both the frontend and backend. - -For frontend packages you'll want to include `esm` output, and for backend -packages `cjs`. Whether to include `types` depends on if you need type -declarations for the package, and also requires `yarn tsc` to have been run -first. - -```text -Usage: backstage-cli build [options] - -Options: - --outputs <formats> List of formats to output [types,cjs,esm] - --minify Minify the generated code - -h, --help display help for command -``` - -## lint - -Scope: `any` - -Lint a package. In addition to the default `eslint` behavior, this command will -include TypeScript files, treat warnings as errors, and default to linting the -entire directory if no specific files are listed. - -```text -Usage: backstage-cli lint [options] - -Options: - --format <format> Lint report output format (default: "eslint-formatter-friendly") - --fix Attempt to automatically fix violations - -h, --help display help for command -``` - -## test - -Scope: `any` - -Run tests, forwarding all unknown options to Jest, and defaulting to watch mode. -When executing the tests, `process.env.NODE_ENV` will be set to `"test"`. - -This command uses a default Jest configuration that is included in the CLI, -which is set up with similar goals for speed, scale, and working within a -monorepo. The configuration sets the `src` as the root directory, enforces the -`.test.` infix for tests, and uses `src/setupTests.ts` as the test setup -location. The included configuration also supports test execution at the root of -a yarn workspaces monorepo by automatically creating one grouped configuration -that includes all packages that have `backstage-cli test` in their package -`test` script. - -For more information about configuration overrides and editor support, see the [Jest Configuration section](./cli-build-system.md#jest-configuration) in the build system documentation. - -```text -Usage: backstage-cli test [options] - -Options: - --backstage-cli-help display help for command -``` - ## config:docs -Scope: `root` - This commands opens up the reference documentation of your apps local configuration schema in the browser. This is useful to get an overview of what configuration values are available to use, a description of what they do and @@ -464,14 +250,12 @@ Usage: backstage-cli config:docs [options] Browse the configuration reference documentation Options: - --package Only include the schema that applies to the given package + --package <name> Only include the schema that applies to the given package -h, --help display help for command ``` ## config:print -Scope: `root` - Print the static configuration, defaulting to reading `app-config.yaml` in the repo root, using schema collected from all local packages in the repo. @@ -497,8 +281,6 @@ Options: ## config:check -Scope: `root` - Validate that static configuration loads and matches schema, defaulting to reading `app-config.yaml` in the repo root and using schema collected from all local packages in the repo. @@ -517,8 +299,6 @@ Options: ## config:schema -Scope: `root` - Dump the configuration schema that was collected from all local packages in the repo. @@ -538,8 +318,6 @@ Options: ## versions:bump -Scope: `root` - Bump all `@backstage` packages to the latest versions. This checks for updates in the package registry, and will update entries both in `yarn.lock` and `package.json` files when necessary. @@ -554,8 +332,6 @@ Options: ## versions:check -Scope: `root` - Validate `@backstage` dependencies within the repo, making sure that there are no duplicates of packages that might lead to breakages. @@ -571,63 +347,8 @@ Options: -h, --help display help for command ``` -## prepack - -Scope: `any` - -This command should be added as `scripts.prepack` in all packages. It enables -packaging- and publish-time overrides for fields inside `packages.json`. - -The checked in version of all packages in a Backstage monorepo are tailored for -local development, and as such `main` and similar fields inside `package.json` -point to development source, i.e. `src/index.ts`. Using this when publishing -would lead to a broken package, since `src/` is not included in the published -package and we instead need to point to files in the `dist/` directory. This -command allows for those fields to be rewritten when needed, and does so by -copying all fields within `publishConfig` to the top-level of each -`package.json`, skipping `access`, `registry`, and `tag`. - -The need for this command may be removed in the future, as this exact method of -overriding fields for publishing is already supported by some package managers. - -```text -Usage: backstage-cli prepack [options] - -Options: - -h, --help display help for command -``` - -## postpack - -Scope: `any` - -This should be added as `scripts.postpack` in all packages. It restores -`package.json` to what it looked like before calling the `prepack` command. - -```text -Usage: backstage-cli postpack [options] - -Options: - -h, --help display help for command -``` - -## clean - -Scope: `any` - -Remove cache and output directories. - -```text -Usage: backstage-cli clean [options] - -Options: - -h, --help display help for command -``` - ## build-workspace -Scope: `any`, `root` - Builds a mirror of the workspace using the packaged production version of each package. This essentially calls `yarn pack` in each included package and unpacks the resulting archive in the target `workspace-dir`. @@ -638,8 +359,6 @@ Usage: backstage-cli build-workspace [options] <workspace-dir> ## create-github-app -Scope: `root` - Creates a GitHub App in your GitHub organization. This is an alternative to token-based [GitHub integration](../integrations/github/locations.md). See [GitHub Apps for Backstage Authentication](../plugins/github-apps.md). @@ -653,8 +372,6 @@ Usage: backstage-cli create-github-app <github-org> ## info -Scope: `root` - Outputs debug information which is useful when opening an issue. Outputs system information, node.js and npm versions, CLI version and type (inside backstage repo or a created app), all `@backstage/*` package dependency versions. diff --git a/docs/local-dev/cli-overview.md b/docs/local-dev/cli-overview.md index 9e6884aab0..74c09d2865 100644 --- a/docs/local-dev/cli-overview.md +++ b/docs/local-dev/cli-overview.md @@ -50,3 +50,4 @@ improve the tooling, as well as to more easily keep the system up to date. - **Bundle** - A collection of the deployment artifacts. The output of the bundling process, which brings a collection of packages into a single collection of deployment artifacts. +- **Package Role** - The declared role of a package, see [package roles](./cli-build-system.md#package-roles). diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index 358dc53852..5b6b81cf10 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -310,7 +310,7 @@ backend: backend: cache: store: redis - connection: user:pass@cache.example.com:6379 + connection: redis://user:pass@cache.example.com:6379 ``` Contributions supporting other cache stores are welcome! diff --git a/docs/publishing.md b/docs/publishing.md index fbbef93493..a0d67b7d24 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -67,12 +67,3 @@ process is used to release an emergency fix as version `6.5.1` in the patch rele - [ ] The fix, which you can likely cherry-pick from your patch branch: `git cherry-pick origin/patch/v1.18.0^` - [ ] An updated `CHANGELOG.md` of all patched packages from the tip of the patch branch, `git checkout origin/patch/v1.18.0 -- {packages,plugins}/*/CHANGELOG.md`. - [ ] A changeset with the message "Applied the fix from version `6.5.1` of this package, which is part of the `v1.18.1` release of Backstage." - - [ ] An entry in `.changeset/patched.json` that sets the current release version to `6.5.1`: - - ```json - { - "currentReleaseVersion": { - "@backstage/plugin-foo": "6.5.1" - } - } - ``` diff --git a/docs/tutorials/configuring-plugin-databases.md b/docs/tutorials/configuring-plugin-databases.md index 5c5f0ffb68..9277bb5028 100644 --- a/docs/tutorials/configuring-plugin-databases.md +++ b/docs/tutorials/configuring-plugin-databases.md @@ -49,29 +49,6 @@ yarn add sqlite3 From an operational perspective, you only need to install drivers for clients that are actively used. -### Database Manager - -Existing Backstage instances should be updated to use `DatabaseManager` from -`@backstage/backend-common` in your `packages/backend/src/index.ts` file, the -`SingleConnectionDatabaseManager` has been deprecated. Import the manager and -update the references as shown below if this is not the case: - -```diff -import { -- SingleConnectionDatabaseManager, -+ DatabaseManager, -} from '@backstage/backend-common'; - -// ... - -function makeCreateEnv(config: Config) { - // ... -- const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); -+ const databaseManager = DatabaseManager.fromConfig(config); - // ... -} -``` - ## Configuration You should set the base database client and connection information in your diff --git a/docs/tutorials/package-role-migration.md b/docs/tutorials/package-role-migration.md new file mode 100644 index 0000000000..10d6eca586 --- /dev/null +++ b/docs/tutorials/package-role-migration.md @@ -0,0 +1,139 @@ +--- +id: package-role-migration +title: Package Role Migration +description: Guide for how to migrate packages to use the new role utility +--- + +The Backstage CLI has introduced the concept of package roles, whose purpose is to +enable more powerful tooling, optimizations, and leaner package configuration. More background and +information about the change can be found in the [original RFC](https://github.com/backstage/backstage/issues/8729) and the [FAQ](#faq) on this page. + +Package roles are implemented through a well-known `"backstage"."role"` field in the +`package.json` of each package. There are a handful of roles defined so far, and it +is not possible to use values outside the [set of predefined roles](../local-dev/cli-build-system.md#package-roles). + +With roles in place in all packages, the Backstage CLI is able to automatically +determine how to handle each package. For example, the different build commands +have been replaced by a single one that instead knows how to build each role. +The test and lint configurations are also selected automatically based on the role, and +a new category of `repo` commands have been introduced in the CLI, which are able +to operate across all packages simultaneously. + +Package roles have been used in the Backstage main repository for a while, and +we now recommend that all Backstage projects are migrated to use package roles. + +## Migration + +In order to make the migration as smooth as possible `@backstage/cli` provides +a number of migration utilities. Using these in combination with some manual review +and optional steps should be all you need to migrate to package roles in most projects. + +Before you begin the migration, make sure you have updated to the most recent version of +the `@backstage/cli`. + +### TL;DR, Step 1-4: + +This is a sorter version of all of the steps below, in case you're in a hurry. + +Run the following commands: + +```sh +yarn backstage-cli migrate package-roles +yarn backstage-cli migrate package-scripts +yarn backstage-cli migrate package-lint-configs +``` + +Have a look at the new commands under `yarn backstage-cli repo`, and switch to them wherever you can. They tend to be much faster compared to their `lerna` equivalents. + +### Step 1 - Add package roles + +The first step is to add the `"backstage"."role"` field to each package. This can of course be done manually, but the following command will attempt to automatically detect the role of each package in your project: + +```sh +yarn backstage-cli migrate package-roles +``` + +The automatic detection is not perfect, so it is recommended to manually review the +roles that were assigned to each package. +You can use the [package role definitions](../local-dev/cli-build-system.md#package-roles) as a reference. + +### Step 2 - Migrate package scripts + +The migration to package roles also introduces a new `package` command category to the CLI. +Each command under the `package` category is designed to be mapped directly to an entry in `"scripts"` in `package.json`. These commands replace the existing commands like `build`, `app:build`, `lint`, and `test`. They look something like this: + +```json +{ + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + ... + } +} +``` + +Every package role has a fixed set of recommended scripts. It is strongly recommended that you use these scripts, as it allows for optimizations in other parts of the CLI. You can migrate to using all of these scripts by running the following command: + +```sh +yarn backstage-cli migrate package-scripts +``` + +The migration command also carries over any existing flags that were being passed in the old scripts. + +If you in the end do not want to use this exact script setup, it is still recommended to migrate to using the `package` commands, as the top-level commands will be deprecated and removed. If you don't want to use package roles either, you can pass an explicit role to some of the package commands, for example `yarn backstage-cli package build --role web-library`. + +### Step 3 - Migrate package ESLint configurations + +An area that has been simplified as part of the move to package roles is the ESLint configuration. Rather than having each package select which configuration they want (and getting it wrong), they now use a shared configuration factory that utilizes the package role. You can read more about the new configuration setup in the [build system documentation](../local-dev/cli-build-system.md#linting). + +To migrate the ESLint configuration of all packages in your project, run the following command: + +```sh +yarn backstage-cli migrate package-lint-configs +``` + +This will migrate all existing `.eslintrc.js` that extend the old configuration from `@backstage/cli`, as well as carry over any additional configuration. + +### Step 4 - Use `backstage-cli repo` + +The Backstage CLI recently introduced a new `repo` command category, which houses commands that operate on an entire monorepo at once. These commands work particularly well once packages have been migrated to use roles, as that allows for some very effective optimizations. It is typically much faster to use these commands compared to using tools like `lerna`, as they're able to avoid the overhead of calling package scripts through `yarn` and can operate on multiple packages at once. You can read more about the `repo` command in the [CLI command documentation](../local-dev/cli-commands.md#repo-build). + +The way to execute this step of the migration is not as well defined as the previous steps, as it depends on what your development and CI/CD setup looks like. Look for the following patterns to replace in your root `package.json` as well as CI/CD setup: + +- Commands that lint the entire repo should be replaced with `yarn backstage-cli repo lint` along with a `--since` flag if needed. For example this: + + ```sh + lerna run lint --since origin/master -- + ``` + + would be replaced by the following: + + ```sh + backstage-cli repo lint --since origin/master + ``` + +- In places where the entire repo is being built, use `yarn backstage-cli repo build`, which also supports the `--since` flag. The migration here is a bit more nuanced as it depends on why you are building all packages. + - If you are building all packages to **verify** that you are able to build them, you most likely want `backstage-cli repo build --all`. The `--all` flag signals that bundled packages like `packages/app` and `packages/backend` should be built as well. Pair this up with a `--since` flag in CI to avoid needing to build all packages. + - If you are building all packages to **publish** them, then `backstage-cli repo build` is enough, as it builds all published packages. + - If you are building all packages to **deploy** them, you likely don't want to use the `repo` command at all, simply call `yarn build` in the packages you want to deploy instead. For example, if you are deploying the backend with a docker host build, it's enough to call `yarn build` inside `packages/backend`. + +## FAQ + +### Why were package roles introduced? + +To keep configuration lean, allow for more utilities and tooling, and to enable optimizations in the build system. You can read more about the reasoning in the [original RFC](https://github.com/backstage/backstage/issues/8729). + +### Do I have to migrate to using package roles? + +Short answer - yes. + +Longer answer - mostly, you can get around having to declare the role of your packages by instead explicitly declaring the role in the command invocation or configuration. For example, the `app:build` command will go away, but you can replace it with `package build --role frontend` if you don't want to declare the role in `package.json` . It is however strongly recommended to declare the package roles. + +### I have a package where none of the existing roles apply + +The `web-library`, `node-library` and `common-library` roles are general purpose roles that should cover most use cases. If you feel like none of those roles work for you, then please open an issue in the [Backstage repo](https://github.com/backstage/backstage) and suggest the addition of a new role. + +### Should I include the role in published packages? + +Yes. While there is nothing that will consume the role at the moment, it is likely that future tooling will be able to provide a better experience for users when published packages include the role. diff --git a/docs/tutorials/switching-sqlite-postgres.md b/docs/tutorials/switching-sqlite-postgres.md index fafd2ad586..8a7f90b21f 100644 --- a/docs/tutorials/switching-sqlite-postgres.md +++ b/docs/tutorials/switching-sqlite-postgres.md @@ -43,9 +43,11 @@ backend: + user: ${POSTGRES_USER} + password: ${POSTGRES_PASSWORD} + # https://node-postgres.com/features/ssl -+ #ssl: require # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require) -+ #ca: # if you have a CA file and want to verify it you can uncomment this section -+ #$file: /ca/server.crt ++ # you can set the sslmode configuration option via the `PGSSLMODE` environment variable ++ # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require) ++ # ssl: ++ # ca: # if you have a CA file and want to verify it you can uncomment this section ++ # $file: /ca/server.crt ``` If you have an `app-config.local.yaml` for local development, a similar update diff --git a/microsite/blog/2022-03-04-backstage-upgrade-helper.md b/microsite/blog/2022-03-04-backstage-upgrade-helper.md new file mode 100644 index 0000000000..a189e28cd5 --- /dev/null +++ b/microsite/blog/2022-03-04-backstage-upgrade-helper.md @@ -0,0 +1,56 @@ +--- +title: Avoid upgrade surprises with Backstage Upgrade Helper +author: Vincenzo Scamporlino, Spotify +authorURL: https://github.com/vinzscam +authorImageURL: https://avatars.githubusercontent.com/u/8433119?v=4 +--- + +![backstage header](assets/22-03-04/backstage-upgrade-helper-post-header.png) + +_TLDR;_ The Spotify team recently announced [Backstage Upgrade Helper](https://backstage.github.io/upgrade-helper): a tool that helps adopters keep their Backstage application up-to-date. **Spoiler alert**: this is also a hidden and heartwarming tale about the beauty of open source innovation. + + + +## Upgrading Backstage was a common pain point + +Without a doubt, one of the most exciting things about Backstage is our community. Step into our daily [Discord conversations](https://discord.gg/sBEF5VkG) and the monthly [Community Sessions](https://www.youtube.com/watch?v=0QMQYSTKAx0) and you’ll find a ton of great ideas, problem-solving, and support for one another. + +During one of these discussions, we realized we all shared the same pain point — upgrading a Backstage application. + +The Backstage open source project is in hyper-growth mode, where dozens of pull requests are merged each day. This means adopters need to go through the upgrade process more often in order to get the latest and greatest features. The Spotify team heard a lot of feedback about how painful and manual this process was — so we decided to tackle this problem during one of our internal Hack Days. + +At Spotify, we run Hack Days every month and a lot of ideas come from those sessions (in fact, the idea to open source Backstage itself [was born on a Hack Day](https://open.spotify.com/episode/332yTwGiILGKTS7dsHCj2P)!). During a brainstorming session, we noticed that another open source community faced a similar upgrading problem years back and came up with a really good solution. + +If you have ever worked on a React Native project, you might immediately recognize what we are referring to: The [React Native Upgrade Helper](https://github.com/react-native-community/upgrade-helper) — a web tool to support React Native developers in upgrading their apps — might have saved hours of your time. It certainly saved us hours of ours. So we looked into the project details and discovered it could fulfill our Backstage needs. + +We started by creating a fork from React Native’s open source project, applying a few changes on top of it to introduce Backstage support, and — _ecco_ — we had a new Backstage Upgrade Helper! In just a few days with pretty minimal effort, we created a product that would save a lot of time for the entire Backstage community. And we had fun at the same time (happy developers make happy code 😁 ). + +## So what is Backstage Upgrade Helper? + +The Backstage Upgrade Helper tool enables adopters to easily upgrade their Backstage app by using the power of git to create a diff between different versions. + +Whenever a new version of Backstage is released, the Helper scaffolds a new dummy Backstage app using the `backstage-create-app` cli utility and checks all the generated files in a specific git branch. After the branch is generated, it gets compared with all the existing ones, which results in generating specific git patches stored in specific files. By selecting the version of the current Backstage release together with the version you want to upgrade to, the UI knows which patch file needs to be picked up. + +So, now you can update your Backstage application in three steps rather than manually reading the changelogs of all the Backstage modules in reverse order. + +### To use the Upgrade Helper, follow these simple steps + +1. Go to the [Backstage Upgrade Helper](https://backstage.github.io/upgrade-helper) and enter your current release and the release you would like to upgrade to. + +2. Press **Show me how to upgrade!** After that, the feature spits out the changes between the two Backstage versions in a split-screen view for easy comparison. + + ![Backstage Upgrade Helper in action](assets/22-03-04/backstage-upgrade-helper.gif) + +3. Apply the suggested changes to your source code to correctly upgrade your app to the selected Backstage version. This will let you directly migrate from the version you’re currently using to the target version and skip all the intermediate steps you would have otherwise gone through manually reading all the changelogs. + +[Watch the Upgrade Helper demo](https://youtu.be/nYjI2j-lWEM?t=410). + +## Open source inspires + +Thus far, the Backstage Upgrade Helper has gotten a lot of good feedback from the community and we’re sure to see awesome contributions in the future. But all the credit behind this idea goes to the React Native community. + +With React Native’s open source contributions, we were able to quickly solve a tough problem for the entire Backstage community. Not only did React Native’s project save us time creating a new product, but it has also saved our adopters time upgrading Backstage. + +This is why we love working in open source. The hard work done for one community has the power to influence and inspire another community. We hope Backstage can do the same and pay it forward to other open source projects. + +For more Backstage Upgrade Helper resources, check out the [UI code](https://github.com/backstage/upgrade-helper) and the [git patches](https://github.com/backstage/upgrade-helper-diff). diff --git a/microsite/blog/assets/22-03-04/backstage-upgrade-helper-post-header.png b/microsite/blog/assets/22-03-04/backstage-upgrade-helper-post-header.png new file mode 100644 index 0000000000..c4bc4fdd25 Binary files /dev/null and b/microsite/blog/assets/22-03-04/backstage-upgrade-helper-post-header.png differ diff --git a/microsite/blog/assets/22-03-04/backstage-upgrade-helper.gif b/microsite/blog/assets/22-03-04/backstage-upgrade-helper.gif new file mode 100644 index 0000000000..eec16600f6 Binary files /dev/null and b/microsite/blog/assets/22-03-04/backstage-upgrade-helper.gif differ diff --git a/microsite/data/plugins/periskop.yaml b/microsite/data/plugins/periskop.yaml new file mode 100644 index 0000000000..cce8d0dde5 --- /dev/null +++ b/microsite/data/plugins/periskop.yaml @@ -0,0 +1,9 @@ +--- +title: Periskop +author: Periskop +authorUrl: https://periskop.io/ +category: Monitoring +description: Periskop is a pull-based, language agnostic exception aggregator for microservice environments. +documentation: https://github.com/backstage/backstage/tree/master/plugins/periskop +iconUrl: https://raw.githubusercontent.com/periskop-dev/periskop/master/docs/assets/periskop-logo.png +npmPackageName: '@backstage/plugin-periskop' diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 5369182013..1d409ae051 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -273,6 +273,7 @@ "Tutorials": [ "tutorials/journey", "tutorials/quickstart-app-plugin", + "tutorials/package-role-migration", "tutorials/migrating-away-from-core", "tutorials/configuring-plugin-databases", "tutorials/switching-sqlite-postgres", diff --git a/mkdocs.yml b/mkdocs.yml index 4671a10b7a..65e1a85bb4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -170,6 +170,7 @@ nav: - Deprecations: 'api/deprecations.md' - Tutorials: - Future developer journey: 'tutorials/journey.md' + - Package Role Migration: 'tutorials/package-role-migration.md' - Migrating away from @backstage/core: 'tutorials/migrating-away-from-core.md' - Adding Custom Plugin to Existing Monorepo App: 'tutorials/quickstart-app-plugin.md' - Switching Backstage from SQLite to PostgreSQL: 'tutorials/switching-sqlite-postgres.md' diff --git a/package.json b/package.json index e7cb42c1c6..a880bb25cc 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "dev": "concurrently \"yarn start\" \"yarn start-backend\"", "start": "yarn workspace example-app start", "start-backend": "yarn workspace example-backend start", - "build": "lerna run build", + "build": "backstage-cli repo build --all", "build:api-reports": "yarn build:api-reports:only --tsc", "build:api-reports:only": "ts-node -T -P scripts/tsconfig.json scripts/api-extractor.ts", "build:api-docs": "yarn build:api-reports --docs", @@ -18,9 +18,9 @@ "diff": "lerna run diff --", "test": "backstage-cli test", "test:all": "lerna run test -- --coverage", - "lint": "lerna run lint --since origin/master --", + "lint": "backstage-cli repo lint --since origin/master", "lint:docs": "node ./scripts/check-docs-quality", - "lint:all": "lerna run lint --", + "lint:all": "backstage-cli repo lint", "lint:type-deps": "node scripts/check-type-dependencies.js", "docker-build": "yarn tsc && yarn workspace example-backend build --build-dependencies && yarn workspace example-backend build-image", "backstage-create": "backstage-cli create --scope backstage --no-private", @@ -45,11 +45,9 @@ ] }, "resolutions": { - "**/@graphql-codegen/cli/**/ws": "^7.4.6", - "**/@roadiehq/**/@backstage/plugin-catalog": "*", - "**/@roadiehq/**/@backstage/catalog-model": "*" + "**/@graphql-codegen/cli/**/ws": "^7.4.6" }, - "version": "0.69.0", + "version": "0.71.0-next.0", "dependencies": { "@manypkg/get-packages": "^1.1.3", "@microsoft/api-documenter": "^7.15.0", @@ -67,7 +65,7 @@ "cross-env": "^7.0.0", "concurrently": "^7.0.0", "eslint-plugin-notice": "^0.9.10", - "fs-extra": "9.1.0", + "fs-extra": "10.0.1", "husky": "^7.0.4", "lerna": "^4.0.0", "lint-staged": "^12.2.0", diff --git a/packages/app-defaults/.eslintrc.js b/packages/app-defaults/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/app-defaults/.eslintrc.js +++ b/packages/app-defaults/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 6fb6c46b76..6231b61631 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/app-defaults +## 0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.1-next.0 + +## 0.2.0 + +### Minor Changes + +- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-app-api@0.6.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/plugin-permission-react@0.3.3 + ## 0.1.9 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index e65d8f53ac..8cab2bdf07 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/app-defaults", "description": "Provides the default wiring of a Backstage App", - "version": "0.1.9", + "version": "0.2.1-next.0", "private": false, "publishConfig": { "access": "public", @@ -33,10 +33,10 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-app-api": "^0.5.4", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-permission-react": "^0.3.2", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-permission-react": "^0.3.3", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -46,8 +46,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@types/jest": "^26.0.7", diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index f26c298fb1..3f5cfc1c58 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -21,16 +21,13 @@ import { ErrorAlerter, GoogleAuth, GithubAuth, - OAuth2, OktaAuth, GitlabAuth, - Auth0Auth, MicrosoftAuth, BitbucketAuth, OAuthRequestManager, WebStorage, UrlPatternDiscovery, - SamlAuth, OneLoginAuth, UnhandledErrorForwarder, AtlassianAuth, @@ -49,16 +46,12 @@ import { oauthRequestApiRef, googleAuthApiRef, githubAuthApiRef, - oauth2ApiRef, oktaAuthApiRef, gitlabAuthApiRef, - auth0AuthApiRef, microsoftAuthApiRef, storageApiRef, configApiRef, - samlAuthApiRef, oneloginAuthApiRef, - oidcAuthApiRef, bitbucketAuthApiRef, atlassianAuthApiRef, } from '@backstage/core-plugin-api'; @@ -197,46 +190,6 @@ export const apis = [ environment: configApi.getOptionalString('auth.environment'), }), }), - createApiFactory({ - api: auth0AuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - Auth0Auth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oauth2ApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: samlAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, configApi }) => - SamlAuth.create({ - discoveryApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), createApiFactory({ api: oneloginAuthApiRef, deps: { @@ -251,25 +204,6 @@ export const apis = [ environment: configApi.getOptionalString('auth.environment'), }), }), - createApiFactory({ - api: oidcAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider: { - id: 'oidc', - title: 'Your Identity Provider', - icon: () => null, - }, - environment: configApi.getOptionalString('auth.environment'), - }), - }), createApiFactory({ api: bitbucketAuthApiRef, deps: { diff --git a/packages/app/.eslintrc.js b/packages/app/.eslintrc.js index 930c98ee1b..e2a53a6ad2 100644 --- a/packages/app/.eslintrc.js +++ b/packages/app/.eslintrc.js @@ -1,11 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], - overrides: [ - { - files: ['**/*.ts?(x)'], - rules: { - 'react/prop-types': 1, - }, - }, - ], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 029b97ec30..114a7f1e2f 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,108 @@ # example-app +## 0.2.68-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.15.2-next.0 + - @backstage/plugin-catalog@0.10.0-next.0 + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/plugin-gcp-projects@0.3.21-next.0 + - @backstage/plugin-scaffolder@0.15.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/plugin-rollbar@0.4.2-next.0 + - @backstage/plugin-catalog-graph@0.2.14-next.0 + - @backstage/plugin-catalog-import@0.8.5-next.0 + - @backstage/plugin-explore@0.3.33-next.0 + - @backstage/plugin-catalog-common@0.2.2-next.0 + - @backstage/plugin-search-common@0.3.1-next.0 + - @backstage/plugin-search@0.7.3-next.0 + - @backstage/plugin-tech-radar@0.5.9-next.0 + - @backstage/plugin-org@0.5.2-next.0 + - @backstage/plugin-techdocs@0.15.1-next.0 + - @backstage/app-defaults@0.2.1-next.0 + - @backstage/integration-react@0.1.25-next.0 + - @backstage/plugin-airbrake@0.3.2-next.0 + - @backstage/plugin-apache-airflow@0.1.10-next.0 + - @backstage/plugin-api-docs@0.8.2-next.0 + - @backstage/plugin-azure-devops@0.1.18-next.0 + - @backstage/plugin-badges@0.2.26-next.0 + - @backstage/plugin-circleci@0.3.2-next.0 + - @backstage/plugin-cloudbuild@0.3.2-next.0 + - @backstage/plugin-code-coverage@0.1.29-next.0 + - @backstage/plugin-cost-insights@0.11.24-next.0 + - @backstage/plugin-github-actions@0.5.2-next.0 + - @backstage/plugin-gocd@0.1.8-next.0 + - @backstage/plugin-graphiql@0.2.34-next.0 + - @backstage/plugin-home@0.4.18-next.0 + - @backstage/plugin-jenkins@0.7.1-next.0 + - @backstage/plugin-kafka@0.3.2-next.0 + - @backstage/plugin-kubernetes@0.6.2-next.0 + - @backstage/plugin-lighthouse@0.3.2-next.0 + - @backstage/plugin-newrelic@0.3.20-next.0 + - @backstage/plugin-newrelic-dashboard@0.1.10-next.0 + - @backstage/plugin-pagerduty@0.3.29-next.0 + - @backstage/plugin-sentry@0.3.40-next.0 + - @backstage/plugin-shortcuts@0.2.3-next.0 + - @backstage/plugin-tech-insights@0.1.12-next.0 + - @backstage/plugin-todo@0.2.4-next.0 + - @backstage/plugin-user-settings@0.4.1-next.0 + +## 0.2.67 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-search@0.7.2 + - @backstage/plugin-techdocs@0.15.0 + - @backstage/plugin-api-docs@0.8.1 + - @backstage/plugin-catalog@0.9.1 + - @backstage/plugin-catalog-graph@0.2.13 + - @backstage/plugin-catalog-import@0.8.4 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/plugin-explore@0.3.32 + - @backstage/plugin-rollbar@0.4.1 + - @backstage/plugin-catalog-common@0.2.0 + - @backstage/plugin-org@0.5.1 + - @backstage/plugin-scaffolder@0.14.0 + - @backstage/core-app-api@0.6.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/cli@0.15.0 + - @backstage/app-defaults@0.2.0 + - @backstage/plugin-user-settings@0.4.0 + - @backstage/plugin-airbrake@0.3.1 + - @backstage/search-common@0.3.0 + - @backstage/plugin-jenkins@0.7.0 + - @backstage/plugin-code-coverage@0.1.28 + - @backstage/plugin-tech-insights@0.1.11 + - @backstage/plugin-azure-devops@0.1.17 + - @backstage/plugin-badges@0.2.25 + - @backstage/plugin-circleci@0.3.1 + - @backstage/plugin-cloudbuild@0.3.1 + - @backstage/plugin-cost-insights@0.11.23 + - @backstage/plugin-github-actions@0.5.1 + - @backstage/plugin-gocd@0.1.7 + - @backstage/plugin-home@0.4.17 + - @backstage/plugin-kafka@0.3.1 + - @backstage/plugin-kubernetes@0.6.1 + - @backstage/plugin-lighthouse@0.3.1 + - @backstage/plugin-newrelic-dashboard@0.1.9 + - @backstage/plugin-pagerduty@0.3.28 + - @backstage/plugin-sentry@0.3.39 + - @backstage/plugin-todo@0.2.3 + - @backstage/integration-react@0.1.24 + - @backstage/plugin-apache-airflow@0.1.9 + - @backstage/plugin-gcp-projects@0.3.20 + - @backstage/plugin-graphiql@0.2.33 + - @backstage/plugin-newrelic@0.3.19 + - @backstage/plugin-shortcuts@0.2.2 + - @backstage/plugin-tech-radar@0.5.8 + - @backstage/plugin-permission-react@0.3.3 + ## 0.2.66 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 25dcbbd945..1d020d20de 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,59 +1,59 @@ { "name": "example-app", - "version": "0.2.66", + "version": "0.2.68-next.0", "private": true, "backstage": { "role": "frontend" }, "bundled": true, "dependencies": { - "@backstage/app-defaults": "^0.1.9", - "@backstage/catalog-model": "^0.11.0", - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-airbrake": "^0.3.0", - "@backstage/plugin-api-docs": "^0.8.0", - "@backstage/plugin-azure-devops": "^0.1.16", - "@backstage/plugin-apache-airflow": "^0.1.8", - "@backstage/plugin-badges": "^0.2.24", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/plugin-catalog-common": "^0.1.4", - "@backstage/plugin-catalog-graph": "^0.2.12", - "@backstage/plugin-catalog-import": "^0.8.3", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/plugin-circleci": "^0.3.0", - "@backstage/plugin-cloudbuild": "^0.3.0", - "@backstage/plugin-code-coverage": "^0.1.27", - "@backstage/plugin-cost-insights": "^0.11.22", - "@backstage/plugin-explore": "^0.3.31", - "@backstage/plugin-gcp-projects": "^0.3.19", - "@backstage/plugin-github-actions": "^0.5.0", - "@backstage/plugin-gocd": "^0.1.6", - "@backstage/plugin-graphiql": "^0.2.32", - "@backstage/plugin-home": "^0.4.16", - "@backstage/plugin-jenkins": "^0.6.0", - "@backstage/plugin-kafka": "^0.3.0", - "@backstage/plugin-kubernetes": "^0.6.0", - "@backstage/plugin-lighthouse": "^0.3.0", - "@backstage/plugin-newrelic": "^0.3.18", - "@backstage/plugin-newrelic-dashboard": "^0.1.8", - "@backstage/plugin-org": "^0.5.0", - "@backstage/plugin-pagerduty": "0.3.27", - "@backstage/plugin-permission-react": "^0.3.2", - "@backstage/plugin-rollbar": "^0.4.0", - "@backstage/plugin-scaffolder": "^0.13.0", - "@backstage/plugin-search": "^0.7.1", - "@backstage/plugin-sentry": "^0.3.38", - "@backstage/plugin-shortcuts": "^0.2.1", - "@backstage/plugin-tech-radar": "^0.5.7", - "@backstage/plugin-techdocs": "^0.14.0", - "@backstage/plugin-todo": "^0.2.2", - "@backstage/plugin-user-settings": "^0.3.21", - "@backstage/search-common": "^0.2.4", - "@backstage/plugin-tech-insights": "^0.1.10", + "@backstage/app-defaults": "^0.2.1-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/integration-react": "^0.1.25-next.0", + "@backstage/plugin-airbrake": "^0.3.2-next.0", + "@backstage/plugin-api-docs": "^0.8.2-next.0", + "@backstage/plugin-azure-devops": "^0.1.18-next.0", + "@backstage/plugin-apache-airflow": "^0.1.10-next.0", + "@backstage/plugin-badges": "^0.2.26-next.0", + "@backstage/plugin-catalog": "^0.10.0-next.0", + "@backstage/plugin-catalog-common": "^0.2.2-next.0", + "@backstage/plugin-catalog-graph": "^0.2.14-next.0", + "@backstage/plugin-catalog-import": "^0.8.5-next.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", + "@backstage/plugin-circleci": "^0.3.2-next.0", + "@backstage/plugin-cloudbuild": "^0.3.2-next.0", + "@backstage/plugin-code-coverage": "^0.1.29-next.0", + "@backstage/plugin-cost-insights": "^0.11.24-next.0", + "@backstage/plugin-explore": "^0.3.33-next.0", + "@backstage/plugin-gcp-projects": "^0.3.21-next.0", + "@backstage/plugin-github-actions": "^0.5.2-next.0", + "@backstage/plugin-gocd": "^0.1.8-next.0", + "@backstage/plugin-graphiql": "^0.2.34-next.0", + "@backstage/plugin-home": "^0.4.18-next.0", + "@backstage/plugin-jenkins": "^0.7.1-next.0", + "@backstage/plugin-kafka": "^0.3.2-next.0", + "@backstage/plugin-kubernetes": "^0.6.2-next.0", + "@backstage/plugin-lighthouse": "^0.3.2-next.0", + "@backstage/plugin-newrelic": "^0.3.20-next.0", + "@backstage/plugin-newrelic-dashboard": "^0.1.10-next.0", + "@backstage/plugin-org": "^0.5.2-next.0", + "@backstage/plugin-pagerduty": "0.3.29-next.0", + "@backstage/plugin-permission-react": "^0.3.3", + "@backstage/plugin-rollbar": "^0.4.2-next.0", + "@backstage/plugin-scaffolder": "^0.15.0-next.0", + "@backstage/plugin-search": "^0.7.3-next.0", + "@backstage/plugin-search-common": "^0.3.1-next.0", + "@backstage/plugin-sentry": "^0.3.40-next.0", + "@backstage/plugin-shortcuts": "^0.2.3-next.0", + "@backstage/plugin-tech-radar": "^0.5.9-next.0", + "@backstage/plugin-techdocs": "^0.15.1-next.0", + "@backstage/plugin-todo": "^0.2.4-next.0", + "@backstage/plugin-user-settings": "^0.4.1-next.0", + "@backstage/plugin-tech-insights": "^0.1.12-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -74,8 +74,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/plugin-permission-react": "^0.3.2", - "@backstage/test-utils": "^0.2.6", + "@backstage/plugin-permission-react": "^0.3.3", + "@backstage/test-utils": "^0.3.0", "@rjsf/core": "^3.2.1", "@testing-library/cypress": "^8.0.2", "@testing-library/jest-dom": "^5.10.1", @@ -96,9 +96,9 @@ "build": "backstage-cli package build", "clean": "backstage-cli package clean", "test": "backstage-cli package test", + "lint": "backstage-cli package lint", "test:e2e": "start-server-and-test start http://localhost:3000 cy:dev", "test:e2e:ci": "start-server-and-test start http://localhost:3000 cy:run", - "lint": "backstage-cli package lint", "cy:dev": "cypress open", "cy:run": "cypress run" }, diff --git a/packages/app/src/components/catalog/EntityPage.test.tsx b/packages/app/src/components/catalog/EntityPage.test.tsx index b68b9e9895..68ce5fb497 100644 --- a/packages/app/src/components/catalog/EntityPage.test.tsx +++ b/packages/app/src/components/catalog/EntityPage.test.tsx @@ -16,15 +16,14 @@ import { EntityLayout } from '@backstage/plugin-catalog'; import { - DefaultStarredEntitiesApi, EntityProvider, starredEntitiesApiRef, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { githubActionsApiRef } from '@backstage/plugin-github-actions'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { MockPermissionApi, - MockStorageApi, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -59,12 +58,7 @@ describe('EntityPage Test', () => { diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 49204d2b5e..5f8c0faf47 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -19,27 +19,12 @@ import { gitlabAuthApiRef, oktaAuthApiRef, githubAuthApiRef, - samlAuthApiRef, microsoftAuthApiRef, oneloginAuthApiRef, - oauth2ApiRef, - oidcAuthApiRef, bitbucketAuthApiRef, } from '@backstage/core-plugin-api'; export const providers = [ - { - id: 'oidc-auth-provider', - title: 'Oidc', - message: 'Sign In using OpenId Connect', - apiRef: oidcAuthApiRef, - }, - { - id: 'oauth2-auth-provider', - title: 'OAuth 2.0', - message: 'Sign In using OAuth 2.0', - apiRef: oauth2ApiRef, - }, { id: 'google-auth-provider', title: 'Google', @@ -70,12 +55,6 @@ export const providers = [ message: 'Sign In using Okta', apiRef: oktaAuthApiRef, }, - { - id: 'saml-auth-provider', - title: 'SAML', - message: 'Sign In using SAML', - apiRef: samlAuthApiRef, - }, { id: 'onelogin-auth-provider', title: 'OneLogin', diff --git a/packages/backend-common/.eslintrc.js b/packages/backend-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/backend-common/.eslintrc.js +++ b/packages/backend-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index ce5c2f0894..64c8034fa4 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/backend-common +## 0.13.0-next.0 + +### Minor Changes + +- ae9d6fb3df: **BREAKING**: + + - Removed the (since way back) deprecated `createDatabase` export, please use `createDatabaseClient` instead. + - Removed the (since way back) deprecated `SingleConnectionDatabaseManager` export, please use `DatabaseManager` instead. + +### Patch Changes + +- ab7cd7d70e: Do some groundwork for supporting the `better-sqlite3` driver, to maybe eventually replace `@vscode/sqlite3` (#9912) +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- aefca2a7e9: add support for ETag at `BitbucketUrlReader.readUrl` +- 3c2bc73901: Use `setupRequestMockHandlers` from `@backstage/backend-test-utils` +- b1aacbf96a: Applied the fix for the `/alpha` entry point resolution that was part of the `v0.70.1` release of Backstage. +- Updated dependencies + - @backstage/config-loader@0.9.7-next.0 + +## 0.12.1 + +### Patch Changes + +- Fixed runtime resolution of the `/alpha` entry point. + +## 0.12.0 + +### Minor Changes + +- 9a0510144f: **BREAKING**: The connection string for `redis` cache store now requires a protocol prefix. + + ```diff + backend: + cache: + store: redis + - connection: user:pass@cache.example.com:6379 + + connection: redis://user:pass@cache.example.com:6379 + ``` + +### Patch Changes + +- 0df6077ab5: DockerContainerRunner.runContainer now automatically removes the container when its execution terminates +- 34af86517c: ensure `apiBaseUrl` being set for Bitbucket integrations, replace hardcoded defaults +- b838717e92: Export FetchUrlReader to facilitate more flexible configuration of the backend. +- Updated dependencies + - @backstage/integration@0.8.0 + ## 0.11.0 ### Minor Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 9b963f3fc9..1b901ea5ac 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -24,7 +24,7 @@ import { isChildPath } from '@backstage/cli-common'; import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { LoadConfigOptionsRemote } from '@backstage/config-loader'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { MergeResult } from 'isomorphic-git'; import { PushResult } from 'isomorphic-git'; import { Readable } from 'stream'; @@ -135,7 +135,7 @@ export class CacheManager { // @public export type CacheManagerOptions = { - logger?: Logger_2; + logger?: Logger; onError?: (err: Error) => void; }; @@ -170,9 +170,6 @@ export class Contexts { ): Context; } -// @public @deprecated -export const createDatabase: typeof createDatabaseClient; - // @public export function createDatabaseClient( dbConfig: Config, @@ -190,7 +187,7 @@ export function createServiceBuilder(_module: NodeModule): ServiceBuilder; // @public export function createStatusCheckRouter(options: { - logger: Logger_2; + logger: Logger; path?: string; statusCheck?: StatusCheck; }): Promise; @@ -230,10 +227,25 @@ export function errorHandler( // @public export type ErrorHandlerOptions = { showStackTraces?: boolean; - logger?: Logger_2; + logger?: Logger; logClientErrors?: boolean; }; +// @public +export class FetchUrlReader implements UrlReader { + static factory: ReaderFactory; + // (undocumented) + read(url: string): Promise; + // (undocumented) + readTree(): Promise; + // (undocumented) + readUrl(url: string, options?: ReadUrlOptions): Promise; + // (undocumented) + search(): Promise; + // (undocumented) + toString(): string; +} + // @public export type FromReadableArrayOptions = Array<{ data: Readable; @@ -285,7 +297,7 @@ export class Git { static fromAuth: (options: { username?: string; password?: string; - logger?: Logger_2; + logger?: Logger; }) => Git; // (undocumented) init(options: { dir: string; defaultBranch?: string }): Promise; @@ -361,7 +373,7 @@ export function isDatabaseConflictError(e: unknown): boolean; // @public export function loadBackendConfig(options: { - logger: Logger_2; + logger: Logger; remote?: LoadConfigOptionsRemote; argv: string[]; }): Promise; @@ -391,7 +403,7 @@ export type PluginEndpointDiscovery = { // @public export type ReaderFactory = (options: { config: Config; - logger: Logger_2; + logger: Logger; treeResponseFactory: ReadTreeResponseFactory; }) => UrlReaderPredicateTuple[]; @@ -468,12 +480,10 @@ export type ReadUrlResponse = { }; // @public -export function requestLoggingHandler(logger?: Logger_2): RequestHandler; +export function requestLoggingHandler(logger?: Logger): RequestHandler; // @public -export type RequestLoggingHandlerFactory = ( - logger?: Logger_2, -) => RequestHandler; +export type RequestLoggingHandlerFactory = (logger?: Logger) => RequestHandler; // @public export function resolvePackagePath(name: string, ...paths: string[]): string; @@ -519,7 +529,7 @@ export class ServerTokenManager implements TokenManager { static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; }, ): ServerTokenManager; // (undocumented) @@ -535,7 +545,7 @@ export type ServiceBuilder = { loadConfig(config: Config): ServiceBuilder; setPort(port: number): ServiceBuilder; setHost(host: string): ServiceBuilder; - setLogger(logger: Logger_2): ServiceBuilder; + setLogger(logger: Logger): ServiceBuilder; enableCors(options: cors.CorsOptions): ServiceBuilder; setHttpsSettings(settings: { certificate: @@ -559,9 +569,6 @@ export type ServiceBuilder = { // @public export function setRootLogger(newLogger: winston.Logger): void; -// @public @deprecated -export const SingleConnectionDatabaseManager: typeof DatabaseManager; - // @public export class SingleHostDiscovery implements PluginEndpointDiscovery { static fromConfig( @@ -622,7 +629,7 @@ export class UrlReaders { // @public export type UrlReadersOptions = { config: Config; - logger: Logger_2; + logger: Logger; factories?: ReaderFactory[]; }; diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 57645d9fda..0c3e3132c0 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -137,7 +137,7 @@ export interface Config { | { store: 'redis'; /** - * A redis connection string in the form `user:pass@host:port`. + * A redis connection string in the form `redis://user:pass@host:port`. * @secret */ connection: string; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index fd5ea443ab..cc14851d94 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.11.0", + "version": "0.13.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -36,9 +36,9 @@ "dependencies": { "@backstage/cli-common": "^0.1.8", "@backstage/config": "^0.1.15", - "@backstage/config-loader": "^0.9.6", + "@backstage/config-loader": "^0.9.7-next.0", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", + "@backstage/integration": "^0.8.0", "@backstage/types": "^0.1.3", "@google-cloud/storage": "^5.8.0", "@manypkg/get-packages": "^1.1.3", @@ -55,7 +55,7 @@ "dockerode": "^3.3.1", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "fs-extra": "9.1.0", + "fs-extra": "10.0.1", "git-url-parse": "^11.6.0", "helmet": "^5.0.2", "isomorphic-git": "^1.8.0", @@ -89,8 +89,8 @@ } }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/backend-test-utils": "^0.1.21-next.0", + "@backstage/cli": "^0.15.2-next.0", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^2.0.0", @@ -106,6 +106,7 @@ "@types/unzipper": "^0.10.3", "@types/webpack-env": "^1.15.2", "aws-sdk-mock": "^5.2.1", + "better-sqlite3": "^7.5.0", "http-errors": "^2.0.0", "jest": "^26.0.1", "mock-fs": "^5.1.0", diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index 660aa82bb0..c6cad2e49b 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -195,14 +195,13 @@ describe('CacheManager', () => { }); it('returns a Redis client when configured', () => { - const redisHostAndPort = '127.0.0.1:6379'; - const expectedHost = `redis://${redisHostAndPort}`; + const redisConnection = 'redis://127.0.0.1:6379'; const manager = CacheManager.fromConfig( new ConfigReader({ backend: { cache: { store: 'redis', - connection: redisHostAndPort, + connection: redisConnection, }, }, }), @@ -218,7 +217,7 @@ describe('CacheManager', () => { expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvRedis); const redis = KeyvRedis as jest.Mock; const mockRedisCalls = redis.mock.calls.splice(-1); - expect(mockRedisCalls[0][0]).toEqual(expectedHost); + expect(mockRedisCalls[0][0]).toEqual(redisConnection); }); describe('connection errors', () => { diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index d63ac6d940..d8652e42d8 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -133,7 +133,7 @@ export class CacheManager { return new Keyv({ namespace: pluginId, ttl: defaultTtl, - store: new KeyvRedis(`redis://${this.connection}`), + store: new KeyvRedis(this.connection), }); } diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index 3aadd30148..503945e594 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -119,7 +119,7 @@ export class DatabaseManager { private getDatabaseName(pluginId: string): string | undefined { const connection = this.getConnectionConfig(pluginId); - if (this.getClientType(pluginId).client === 'sqlite3') { + if (this.getClientType(pluginId).client.includes('sqlite3')) { const sqliteFilename: string | undefined = ( connection as Knex.Sqlite3ConnectionConfig ).filename; @@ -224,7 +224,7 @@ export class DatabaseManager { ); if ( - client === 'sqlite3' && + client.includes('sqlite3') && 'filename' in baseConnection && baseConnection.filename !== ':memory:' ) { diff --git a/packages/backend-common/src/database/SingleConnection.test.ts b/packages/backend-common/src/database/SingleConnection.test.ts deleted file mode 100644 index 46d7376d25..0000000000 --- a/packages/backend-common/src/database/SingleConnection.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConfigReader } from '@backstage/config'; -import { createDatabaseClient, ensureDatabaseExists } from './connection'; -import { SingleConnectionDatabaseManager } from './SingleConnection'; - -jest.mock('./connection', () => ({ - ...jest.requireActual('./connection'), - createDatabaseClient: jest.fn(), - ensureDatabaseExists: jest.fn(), -})); - -describe('SingleConnectionDatabaseManager', () => { - const defaultConfigOptions = { - backend: { - database: { - client: 'pg', - connection: { - host: 'localhost', - user: 'foo', - password: 'bar', - database: 'foodb', - }, - }, - }, - }; - const defaultConfig = () => new ConfigReader(defaultConfigOptions); - - // This is similar to the ts-jest `mocked` helper. - const mocked = (f: Function) => f as jest.Mock; - - afterEach(() => jest.resetAllMocks()); - - describe('SingleConnectionDatabaseManager.fromConfig', () => { - it('accesses the backend.database key', () => { - const config = defaultConfig(); - const getConfig = jest.spyOn(config, 'getConfig'); - - SingleConnectionDatabaseManager.fromConfig(config); - - expect(getConfig.mock.calls[0][0]).toEqual('backend.database'); - }); - }); - - describe('SingleConnectionDatabaseManager.forPlugin', () => { - const manager = SingleConnectionDatabaseManager.fromConfig(defaultConfig()); - - it('connects to a database scoped to the plugin', async () => { - const pluginId = 'test1'; - await manager.forPlugin(pluginId).getClient(); - - expect(mocked(createDatabaseClient)).toHaveBeenCalledTimes(1); - - const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); - const callArgs = mockCalls[0]; - expect(callArgs[1].connection.database).toEqual( - `backstage_plugin_${pluginId}`, - ); - }); - - it('provides different plugins different databases', async () => { - const plugin1Id = 'test1'; - const plugin2Id = 'test2'; - await manager.forPlugin(plugin1Id).getClient(); - await manager.forPlugin(plugin2Id).getClient(); - - expect(mocked(createDatabaseClient)).toHaveBeenCalledTimes(2); - - const mockCalls = mocked(createDatabaseClient).mock.calls; - const plugin1CallArgs = mockCalls[0]; - const plugin2CallArgs = mockCalls[1]; - expect(plugin1CallArgs[1].connection.database).not.toEqual( - plugin2CallArgs[1].connection.database, - ); - }); - - it('ensure plugin database is created', async () => { - await manager.forPlugin('test').getClient(); - const mockCalls = mocked(ensureDatabaseExists).mock.calls.splice(-1); - const [_, database] = mockCalls[0]; - - expect(database).toEqual('backstage_plugin_test'); - }); - }); -}); diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 7fc9df060d..5f487886b4 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -23,7 +23,13 @@ import { DatabaseConnector } from './types'; import { mysqlConnector, pgConnector, sqlite3Connector } from './connectors'; -type DatabaseClient = 'pg' | 'sqlite3' | 'mysql' | 'mysql2' | string; +type DatabaseClient = + | 'pg' + | 'better-sqlite3' + | 'sqlite3' + | 'mysql' + | 'mysql2' + | string; /** * Mapping of client type to supported database connectors @@ -33,6 +39,7 @@ type DatabaseClient = 'pg' | 'sqlite3' | 'mysql' | 'mysql2' | string; */ const ConnectorMapping: Record = { pg: pgConnector, + 'better-sqlite3': sqlite3Connector, sqlite3: sqlite3Connector, mysql: mysqlConnector, mysql2: mysqlConnector, @@ -57,14 +64,6 @@ export function createDatabaseClient( ); } -/** - * Alias for {@link createDatabaseClient} - * - * @public - * @deprecated Use createDatabaseClient instead - */ -export const createDatabase = createDatabaseClient; - /** * Ensures that the given databases all exist, creating them if they do not. * diff --git a/packages/backend-common/src/database/connectors/sqlite3.test.ts b/packages/backend-common/src/database/connectors/sqlite3.test.ts index cfc9f61527..d751d895d0 100644 --- a/packages/backend-common/src/database/connectors/sqlite3.test.ts +++ b/packages/backend-common/src/database/connectors/sqlite3.test.ts @@ -101,3 +101,84 @@ describe('sqlite3', () => { }); }); }); + +describe('better-sqlite3', () => { + const createConfig = (connection: any) => + new ConfigReader({ client: 'better-sqlite3', connection }); + + describe('buildSqliteDatabaseConfig', () => { + it('builds an in-memory connection', () => { + expect(buildSqliteDatabaseConfig(createConfig(':memory:'))).toEqual({ + client: 'better-sqlite3', + connection: { filename: ':memory:' }, + useNullAsDefault: true, + }); + }); + + it('builds an in-memory connection by override with filename', () => { + expect( + buildSqliteDatabaseConfig( + createConfig(path.join('path', 'to', 'foo')), + { connection: ':memory:' }, + ), + ).toEqual({ + client: 'better-sqlite3', + connection: { filename: ':memory:' }, + useNullAsDefault: true, + }); + }); + + it('builds a persistent connection, normalize config with filename', () => { + expect( + buildSqliteDatabaseConfig(createConfig(path.join('path', 'to', 'foo'))), + ).toEqual({ + client: 'better-sqlite3', + connection: { filename: path.join('path', 'to', 'foo') }, + useNullAsDefault: true, + }); + }); + + it('builds a persistent connection', () => { + expect( + buildSqliteDatabaseConfig( + createConfig({ + filename: path.join('path', 'to', 'foo'), + }), + ), + ).toEqual({ + client: 'better-sqlite3', + connection: { + filename: path.join('path', 'to', 'foo'), + }, + useNullAsDefault: true, + }); + }); + + it('replaces the connection with an override', () => { + expect( + buildSqliteDatabaseConfig(createConfig(':memory:'), { + connection: { filename: path.join('path', 'to', 'foo') }, + }), + ).toEqual({ + client: 'better-sqlite3', + connection: { + filename: path.join('path', 'to', 'foo'), + }, + useNullAsDefault: true, + }); + }); + }); + + describe('createSqliteDatabaseClient', () => { + it('creates an in memory knex instance', () => { + expect( + createSqliteDatabaseClient( + createConfig({ + client: 'better-sqlite3', + connection: ':memory:', + }), + ), + ).toBeTruthy(); + }); + }); +}); diff --git a/packages/backend-common/src/database/connectors/sqlite3.ts b/packages/backend-common/src/database/connectors/sqlite3.ts index 8e47720f82..6426005f0e 100644 --- a/packages/backend-common/src/database/connectors/sqlite3.ts +++ b/packages/backend-common/src/database/connectors/sqlite3.ts @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import path from 'path'; - -import { ensureDirSync } from 'fs-extra'; -import knexFactory, { Knex } from 'knex'; import { Config } from '@backstage/config'; +import { ensureDirSync } from 'fs-extra'; +import knexFactory, { Knex } from 'knex'; +import path from 'path'; import { mergeDatabaseConfig } from '../config'; import { DatabaseConnector } from '../types'; diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index 2a820f7367..0dde1ec239 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -14,18 +14,13 @@ * limitations under the License. */ -export * from './SingleConnection'; export * from './DatabaseManager'; /* * Undocumented API surface from connection is being reduced for future deprecation. * Avoid exporting additional symbols. */ -export { - createDatabaseClient, - createDatabase, - ensureDatabaseExists, -} from './connection'; +export { createDatabaseClient, ensureDatabaseExists } from './connection'; export type { PluginDatabaseManager } from './types'; export { isDatabaseConflictError } from './util'; diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index 23de8389ad..2eb7a60095 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -19,7 +19,7 @@ import { AzureIntegration, readAzureIntegrationConfig, } from '@backstage/integration'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index ea8e4626be..eff982581b 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -19,7 +19,7 @@ import { BitbucketIntegration, readBitbucketIntegrationConfig, } from '@backstage/integration'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; @@ -75,20 +75,74 @@ describe('BitbucketUrlReader', () => { setupRequestMockHandlers(worker); describe('readUrl', () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (_, res, ctx) => res(ctx.status(200), ctx.body('foo')), - ), - ); + it('should be able to readUrl without ETag', async () => { + worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', + (req, res, ctx) => { + expect(req.headers.get('If-None-Match')).toBeNull(); + return res( + ctx.status(200), + ctx.body('foo'), + ctx.set('ETag', 'etag-value'), + ); + }, + ), + ); - it('should be able to readUrl', async () => { const result = await bitbucketProcessor.readUrl( 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', ); const buffer = await result.buffer(); expect(buffer.toString()).toBe('foo'); }); + + it('should be able to readUrl with matching ETag', async () => { + worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', + (req, res, ctx) => { + expect(req.headers.get('If-None-Match')).toBe( + 'matching-etag-value', + ); + return res(ctx.status(304)); + }, + ), + ); + + await expect( + bitbucketProcessor.readUrl( + 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', + { etag: 'matching-etag-value' }, + ), + ).rejects.toThrow(NotModifiedError); + }); + + it('should be able to readUrl without matching ETag', async () => { + worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', + (req, res, ctx) => { + expect(req.headers.get('If-None-Match')).toBe( + 'previous-etag-value', + ); + return res( + ctx.status(200), + ctx.body('foo'), + ctx.set('ETag', 'new-etag-value'), + ); + }, + ), + ); + + const result = await bitbucketProcessor.readUrl( + 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', + { etag: 'previous-etag-value' }, + ); + const buffer = await result.buffer(); + expect(buffer.toString()).toBe('foo'); + expect(result.etag).toBe('new-etag-value'); + }); }); describe('read', () => { @@ -270,22 +324,6 @@ describe('BitbucketUrlReader', () => { expect(response.etag).toBe('12ab34cd56ef'); }); - - it('should throw error when apiBaseUrl is missing', () => { - expect(() => { - /* eslint-disable no-new */ - new BitbucketUrlReader( - new BitbucketIntegration( - readBitbucketIntegrationConfig( - new ConfigReader({ - host: 'bitbucket.mycompany.net', - }), - ), - ), - { treeResponseFactory }, - ); - }).toThrowError('must configure an explicit apiBaseUrl'); - }); }); describe('search hosted', () => { diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 2006637545..df22f1633f 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -62,14 +62,9 @@ export class BitbucketUrlReader implements UrlReader { private readonly integration: BitbucketIntegration, private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, ) { - const { host, apiBaseUrl, token, username, appPassword } = - integration.config; + const { host, token, username, appPassword } = integration.config; - if (!apiBaseUrl) { - throw new Error( - `Bitbucket integration for '${host}' must configure an explicit apiBaseUrl`, - ); - } else if (!token && username && !appPassword) { + if (!token && username && !appPassword) { throw new Error( `Bitbucket integration for '${host}' has configured a username but is missing a required appPassword.`, ); @@ -85,15 +80,17 @@ export class BitbucketUrlReader implements UrlReader { url: string, options?: ReadUrlOptions, ): Promise { - // TODO: etag is not supported yet - const { signal } = options ?? {}; + const { etag, signal } = options ?? {}; const bitbucketUrl = getBitbucketFileFetchUrl(url, this.integration.config); const requestOptions = getBitbucketRequestOptions(this.integration.config); let response: Response; try { response = await fetch(bitbucketUrl.toString(), { - ...requestOptions, + headers: { + ...requestOptions.headers, + ...(etag && { 'If-None-Match': etag }), + }, // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can be @@ -106,9 +103,14 @@ export class BitbucketUrlReader implements UrlReader { throw new Error(`Unable to read ${url}, ${e}`); } + if (response.status === 304) { + throw new NotModifiedError(); + } + if (response.ok) { return { buffer: async () => Buffer.from(await response.arrayBuffer()), + etag: response.headers.get('ETag') ?? undefined, }; } diff --git a/packages/backend-common/src/reading/FetchUrlReader.test.ts b/packages/backend-common/src/reading/FetchUrlReader.test.ts index 9d6edee765..d2fbbab668 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.test.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.test.ts @@ -16,7 +16,7 @@ import { ConfigReader } from '@backstage/config'; import { NotFoundError, NotModifiedError } from '@backstage/errors'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { getVoidLogger } from '../logging'; diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index ff73718d50..8e59b8d902 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -20,7 +20,7 @@ import { GitHubIntegration, readGitHubIntegrationConfig, } from '@backstage/integration'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index c8a7e17ae3..f422820a29 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -15,7 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; diff --git a/packages/backend-common/src/reading/index.ts b/packages/backend-common/src/reading/index.ts index 2c3394e4fc..9c29bc93fe 100644 --- a/packages/backend-common/src/reading/index.ts +++ b/packages/backend-common/src/reading/index.ts @@ -19,6 +19,7 @@ export { BitbucketUrlReader } from './BitbucketUrlReader'; export { GithubUrlReader } from './GithubUrlReader'; export { GitlabUrlReader } from './GitlabUrlReader'; export { AwsS3UrlReader } from './AwsS3UrlReader'; +export { FetchUrlReader } from './FetchUrlReader'; export type { FromReadableArrayOptions, ReaderFactory, diff --git a/packages/backend-common/src/util/DockerContainerRunner.test.ts b/packages/backend-common/src/util/DockerContainerRunner.test.ts index 8833e4518b..3208679d1f 100644 --- a/packages/backend-common/src/util/DockerContainerRunner.test.ts +++ b/packages/backend-common/src/util/DockerContainerRunner.test.ts @@ -115,6 +115,7 @@ describe('DockerContainerRunner', () => { Env: envVarsArray, WorkingDir: workingDir, HostConfig: { + AutoRemove: true, Binds: expect.arrayContaining([ `${path.join(rootDir, 'input')}:/input`, `${path.join(rootDir, 'output')}:/output`, @@ -207,6 +208,7 @@ describe('DockerContainerRunner', () => { logStream, expect.objectContaining({ HostConfig: { + AutoRemove: true, Binds: [], }, Volumes: {}, diff --git a/packages/backend-common/src/util/DockerContainerRunner.ts b/packages/backend-common/src/util/DockerContainerRunner.ts index 522328c2ec..4d4684c814 100644 --- a/packages/backend-common/src/util/DockerContainerRunner.ts +++ b/packages/backend-common/src/util/DockerContainerRunner.ts @@ -105,6 +105,7 @@ export class DockerContainerRunner implements ContainerRunner { await this.dockerClient.run(imageName, args, logStream, { Volumes, HostConfig: { + AutoRemove: true, Binds, }, ...(workingDir ? { WorkingDir: workingDir } : {}), diff --git a/packages/backend-tasks/.eslintrc.js b/packages/backend-tasks/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/backend-tasks/.eslintrc.js +++ b/packages/backend-tasks/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index 546d4a8187..2f41b37562 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/backend-tasks +## 0.2.0-next.0 + +### Minor Changes + +- 9461f73643: **BREAKING**: The `TaskDefinition` type has been removed, and replaced by the equal pair `TaskScheduleDefinition` and `TaskInvocationDefinition`. The interface for `PluginTaskScheduler.scheduleTask` stays effectively unchanged, so this only affects you if you use the actual types directly. + + Added the method `PluginTaskScheduler.createTaskSchedule`, which returns a `TaskSchedule` wrapper that is convenient to pass down into classes that want to control their task invocations while the caller wants to retain control of the actual schedule chosen. + +### Patch Changes + +- ab7cd7d70e: Do some groundwork for supporting the `better-sqlite3` driver, to maybe eventually replace `@vscode/sqlite3` (#9912) +- 7290dda9d4: Relaxed the task ID requirement to now support any non-empty string +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + +## 0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.1.9 ### Patch Changes diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index eeb6412840..98199a55b0 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -7,21 +7,14 @@ import { AbortSignal as AbortSignal_2 } from 'node-abort-controller'; import { Config } from '@backstage/config'; import { DatabaseManager } from '@backstage/backend-common'; import { Duration } from 'luxon'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // @public export interface PluginTaskScheduler { - scheduleTask(task: TaskDefinition): Promise; -} - -// @public -export interface TaskDefinition { - fn: TaskFunction; - frequency: Duration; - id: string; - initialDelay?: Duration; - signal?: AbortSignal_2; - timeout: Duration; + createScheduledTaskRunner(schedule: TaskScheduleDefinition): TaskRunner; + scheduleTask( + task: TaskScheduleDefinition & TaskInvocationDefinition, + ): Promise; } // @public @@ -29,16 +22,35 @@ export type TaskFunction = | ((abortSignal: AbortSignal_2) => void | Promise) | (() => void | Promise); +// @public +export interface TaskInvocationDefinition { + fn: TaskFunction; + id: string; + signal?: AbortSignal_2; +} + +// @public +export interface TaskRunner { + run(task: TaskInvocationDefinition): Promise; +} + +// @public +export interface TaskScheduleDefinition { + frequency: Duration; + initialDelay?: Duration; + timeout: Duration; +} + // @public export class TaskScheduler { - constructor(databaseManager: DatabaseManager, logger: Logger_2); + constructor(databaseManager: DatabaseManager, logger: Logger); forPlugin(pluginId: string): PluginTaskScheduler; // (undocumented) static fromConfig( config: Config, options?: { databaseManager?: DatabaseManager; - logger?: Logger_2; + logger?: Logger; }, ): TaskScheduler; } diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 215fafe970..b05a740e65 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-tasks", "description": "Common distributed task management library for Backstage backends", - "version": "0.1.9", + "version": "0.2.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -33,7 +33,7 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@backstage/types": "^0.1.3", @@ -47,8 +47,8 @@ "zod": "^3.9.5" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.19", - "@backstage/cli": "^0.14.1", + "@backstage/backend-test-utils": "^0.1.21-next.0", + "@backstage/cli": "^0.15.2-next.0", "jest": "^26.0.1", "wait-for-expect": "^3.0.2" }, diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts index e387b85413..993041bed4 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts @@ -59,4 +59,31 @@ describe('PluginTaskManagerImpl', () => { 60_000, ); }); + + // This is just to test the wrapper code; most of the actual tests are in + // TaskWorker.test.ts + describe('createScheduledTaskRunner', () => { + it.each(databases.eachSupportedId())( + 'can run the happy path, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const fn = jest.fn(); + await manager + .createScheduledTaskRunner({ + timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), + }) + .run({ + id: 'task1', + fn, + }); + + await waitForExpect(() => { + expect(fn).toBeCalled(); + }); + }, + 60_000, + ); + }); }); diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts index 93975bc327..8d50298ffd 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts @@ -17,7 +17,12 @@ import { Knex } from 'knex'; import { Logger } from 'winston'; import { TaskWorker } from './TaskWorker'; -import { PluginTaskScheduler, TaskDefinition } from './types'; +import { + PluginTaskScheduler, + TaskInvocationDefinition, + TaskRunner, + TaskScheduleDefinition, +} from './types'; import { validateId } from './util'; /** @@ -29,7 +34,9 @@ export class PluginTaskSchedulerImpl implements PluginTaskScheduler { private readonly logger: Logger, ) {} - async scheduleTask(task: TaskDefinition): Promise { + async scheduleTask( + task: TaskScheduleDefinition & TaskInvocationDefinition, + ): Promise { validateId(task.id); const knex = await this.databaseFactory(); @@ -47,4 +54,12 @@ export class PluginTaskSchedulerImpl implements PluginTaskScheduler { }, ); } + + createScheduledTaskRunner(schedule: TaskScheduleDefinition): TaskRunner { + return { + run: async task => { + await this.scheduleTask({ ...task, ...schedule }); + }, + }; + } } diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index 991d97b956..5d33bfa7a1 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -229,10 +229,9 @@ export class TaskWorker { // leaning on the database as a central clock source const dbNull = this.knex.raw('null'); const dt = Duration.fromISO(recurringAtMostEveryDuration).as('seconds'); - const nextRun = - this.knex.client.config.client === 'sqlite3' - ? this.knex.raw('datetime(next_run_start_at, ?)', [`+${dt} seconds`]) - : this.knex.raw(`next_run_start_at + interval '${dt} seconds'`); + const nextRun = this.knex.client.config.client.includes('sqlite3') + ? this.knex.raw('datetime(next_run_start_at, ?)', [`+${dt} seconds`]) + : this.knex.raw(`next_run_start_at + interval '${dt} seconds'`); const rows = await this.knex(DB_TASKS_TABLE) .where('id', '=', this.taskId) diff --git a/packages/backend-tasks/src/tasks/index.ts b/packages/backend-tasks/src/tasks/index.ts index 9e0a06f71c..f6695a8d73 100644 --- a/packages/backend-tasks/src/tasks/index.ts +++ b/packages/backend-tasks/src/tasks/index.ts @@ -17,6 +17,8 @@ export { TaskScheduler } from './TaskScheduler'; export type { PluginTaskScheduler, - TaskDefinition, TaskFunction, + TaskInvocationDefinition, + TaskRunner, + TaskScheduleDefinition, } from './types'; diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index 4693af7ef3..35b5598700 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -31,27 +31,11 @@ export type TaskFunction = | (() => void | Promise); /** - * Options that apply to the invocation of a given task. + * Options that control the scheduling of a task. * * @public */ -export interface TaskDefinition { - /** - * A unique ID (within the scope of the plugin) for the task. - */ - id: string; - - /** - * The actual task function to be invoked regularly. - */ - fn: TaskFunction; - - /** - * An abort signal that, when triggered, will stop the recurring execution of - * the task. - */ - signal?: AbortSignal; - +export interface TaskScheduleDefinition { /** * The maximum amount of time that a single task invocation can take, before * it's considered timed out and gets "released" such that a new invocation @@ -91,6 +75,43 @@ export interface TaskDefinition { initialDelay?: Duration; } +/** + * Options that apply to the invocation of a given task. + * + * @public + */ +export interface TaskInvocationDefinition { + /** + * A unique ID (within the scope of the plugin) for the task. + */ + id: string; + + /** + * The actual task function to be invoked regularly. + */ + fn: TaskFunction; + + /** + * An abort signal that, when triggered, will stop the recurring execution of + * the task. + */ + signal?: AbortSignal; +} + +/** + * A previously prepared task schedule, ready to be invoked. + * + * @public + */ +export interface TaskRunner { + /** + * Takes the schedule and executes an actual task using it. + * + * @param task - The actual runtime properties of the task + */ + run(task: TaskInvocationDefinition): Promise; +} + /** * Deals with the scheduling of distributed tasks, for a given plugin. * @@ -99,15 +120,34 @@ export interface TaskDefinition { export interface PluginTaskScheduler { /** * Schedules a task function for coordinated exclusive invocation across - * workers. + * workers. This convenience method performs both the scheduling and + * invocation in one go. + * + * @remarks * * If the task was already scheduled since before by us or by another party, * its options are just overwritten with the given options, and things * continue from there. * - * @param definition - The task definition + * @param task - The task definition */ - scheduleTask(task: TaskDefinition): Promise; + scheduleTask( + task: TaskScheduleDefinition & TaskInvocationDefinition, + ): Promise; + + /** + * Creates a scheduled but dormant recurring task, ready to be launched at a + * later time. + * + * @remarks + * + * This method is useful for pre-creating a schedule in outer code to be + * passed into an inner implementation, such that the outer code controls + * scheduling while inner code controls implementation. + * + * @param schedule - The task schedule + */ + createScheduledTaskRunner(schedule: TaskScheduleDefinition): TaskRunner; } function isValidOptionalDurationString(d: string | undefined): boolean { diff --git a/packages/backend-tasks/src/tasks/util.test.ts b/packages/backend-tasks/src/tasks/util.test.ts index f74669a7f4..614de8c6c8 100644 --- a/packages/backend-tasks/src/tasks/util.test.ts +++ b/packages/backend-tasks/src/tasks/util.test.ts @@ -20,14 +20,14 @@ import { delegateAbortController, sleep, validateId } from './util'; describe('util', () => { describe('validateId', () => { - it.each(['a', 'a_b', 'ab123c_2'])( + it.each(['a', 'a_b', 'ab123c_2', 'a!', 'A', 'a-b', 'a.b', '_a', 'a_'])( 'accepts valid inputs, %p', async input => { expect(validateId(input)).toBeUndefined(); }, ); - it.each(['', 'a!', 'A', 'a-b', 'a.b', '_a', 'a_', null, Symbol('a')])( + it.each(['', null, Symbol('a')])( 'rejects invalid inputs, %p', async input => { expect(() => validateId(input as any)).toThrow(); diff --git a/packages/backend-tasks/src/tasks/util.ts b/packages/backend-tasks/src/tasks/util.ts index 0509f29363..34280cc2d4 100644 --- a/packages/backend-tasks/src/tasks/util.ts +++ b/packages/backend-tasks/src/tasks/util.ts @@ -19,12 +19,10 @@ import { Knex } from 'knex'; import { DateTime, Duration } from 'luxon'; import { AbortController, AbortSignal } from 'node-abort-controller'; -// Keep the IDs compatible with e.g. Prometheus +// Keep the IDs compatible with e.g. Prometheus labels export function validateId(id: string) { - if (typeof id !== 'string' || !/^[a-z0-9]+(?:_[a-z0-9]+)*$/.test(id)) { - throw new InputError( - `${id} is not a valid ID, expected string of lowercase characters and digits separated by underscores`, - ); + if (typeof id !== 'string' || !id.trim()) { + throw new InputError(`${id} is not a valid ID, expected non-empty string`); } } @@ -40,7 +38,7 @@ export function nowPlus(duration: Duration | undefined, knex: Knex) { if (!seconds) { return knex.fn.now(); } - return knex.client.config.client === 'sqlite3' + return knex.client.config.client.includes('sqlite3') ? knex.raw(`datetime('now', ?)`, [`${seconds} seconds`]) : knex.raw(`now() + interval '${seconds} seconds'`); } diff --git a/packages/backend-test-utils/.eslintrc.js b/packages/backend-test-utils/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/backend-test-utils/.eslintrc.js +++ b/packages/backend-test-utils/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 03f564074b..a7dbcbd51f 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/backend-test-utils +## 0.1.21-next.0 + +### Patch Changes + +- ab7cd7d70e: Do some groundwork for supporting the `better-sqlite3` driver, to maybe eventually replace `@vscode/sqlite3` (#9912) +- 3c2bc73901: Add `setupRequestMockHandlers` which sets up a good `msw` server foundation, copied from `@backstage/test-utils` which is a frontend-only package and should not be used from backends. +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/cli@0.15.2-next.0 + +## 0.1.20 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/cli@0.15.0 + ## 0.1.19 ### Patch Changes diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index f31a2675b9..cba8953432 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -8,6 +8,13 @@ import { Knex } from 'knex'; // @public (undocumented) export function isDockerDisabledForTests(): boolean; +// @public +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void; + // @public export type TestDatabaseId = | 'POSTGRES_13' diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 2cc3b8a07b..d5e0feac35 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.1.19", + "version": "0.1.21-next.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -34,18 +34,19 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/cli": "^0.14.1", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/cli": "^0.15.2-next.0", "@backstage/config": "^0.1.15", "@vscode/sqlite3": "^5.0.7", "knex": "^1.0.2", + "msw": "^0.35.0", "mysql2": "^2.2.5", "pg": "^8.3.0", "testcontainers": "^8.1.2", "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "jest": "^26.0.1" }, "files": [ diff --git a/packages/backend-test-utils/src/database/TestDatabases.ts b/packages/backend-test-utils/src/database/TestDatabases.ts index 243c6bbe52..a7f5ceb344 100644 --- a/packages/backend-test-utils/src/database/TestDatabases.ts +++ b/packages/backend-test-utils/src/database/TestDatabases.ts @@ -182,6 +182,7 @@ export class TestDatabases { return this.initPostgres(properties); case 'mysql2': return this.initMysql(properties); + case 'better-sqlite3': case 'sqlite3': return this.initSqlite(properties); default: @@ -240,13 +241,13 @@ export class TestDatabases { } private async initSqlite( - _properties: TestDatabaseProperties, + properties: TestDatabaseProperties, ): Promise { const databaseManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { - client: 'sqlite3', + client: properties.driver, connection: ':memory:', }, }, diff --git a/packages/backend-test-utils/src/database/types.ts b/packages/backend-test-utils/src/database/types.ts index d405f73738..5aba48952f 100644 --- a/packages/backend-test-utils/src/database/types.ts +++ b/packages/backend-test-utils/src/database/types.ts @@ -40,6 +40,7 @@ export type Instance = { databaseManager: DatabaseManager; connections: Array; }; + export const allDatabases: Record = Object.freeze({ POSTGRES_13: { diff --git a/packages/backend-test-utils/src/index.ts b/packages/backend-test-utils/src/index.ts index ee66b230a8..5fef4e6b55 100644 --- a/packages/backend-test-utils/src/index.ts +++ b/packages/backend-test-utils/src/index.ts @@ -21,4 +21,5 @@ */ export * from './database'; +export * from './msw'; export * from './util'; diff --git a/packages/backend-test-utils/src/msw/index.ts b/packages/backend-test-utils/src/msw/index.ts new file mode 100644 index 0000000000..29cbdf854a --- /dev/null +++ b/packages/backend-test-utils/src/msw/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { setupRequestMockHandlers } from './setupRequestMockHandlers'; diff --git a/plugins/catalog-react/src/hooks/useEntityCompoundName.ts b/packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts similarity index 60% rename from plugins/catalog-react/src/hooks/useEntityCompoundName.ts rename to packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts index 481c18f8a0..625ef0e0fc 100644 --- a/plugins/catalog-react/src/hooks/useEntityCompoundName.ts +++ b/packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts @@ -13,15 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { entityRouteRef } from '../routes'; -import { useRouteRefParams } from '@backstage/core-plugin-api'; /** - * Grabs entity kind, namespace, and name from the location + * Sets up handlers for request mocking * @public - * @deprecated use {@link @backstage/core-plugin-api#useRouteRefParams} instead + * @param worker - service worker */ -export const useEntityCompoundName = () => { - const { kind, namespace, name } = useRouteRefParams(entityRouteRef); - return { kind, namespace, name }; -}; +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}) { + beforeAll(() => worker.listen({ onUnhandledRequest: 'error' })); + afterAll(() => worker.close()); + afterEach(() => worker.resetHandlers()); +} diff --git a/packages/backend/.eslintrc.js b/packages/backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/backend/.eslintrc.js +++ b/packages/backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 227b8ee231..d6500304c0 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,81 @@ # example-backend +## 0.2.68-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/backend-tasks@0.2.0-next.0 + - @backstage/plugin-app-backend@0.3.29-next.0 + - @backstage/plugin-auth-backend@0.12.1-next.0 + - @backstage/plugin-catalog-backend@0.24.0-next.0 + - @backstage/plugin-scaffolder-backend@0.18.0-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.3.4-next.0 + - @backstage/plugin-kubernetes-backend@0.4.12-next.0 + - @backstage/plugin-rollbar-backend@0.1.26-next.0 + - @backstage/plugin-techdocs-backend@0.14.2-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/plugin-badges-backend@0.1.23-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@0.1.1-next.0 + - @backstage/plugin-search-backend-module-pg@0.3.1-next.0 + - @backstage/plugin-search-backend-node@0.5.1-next.0 + - @backstage/plugin-search-backend@0.4.7-next.0 + - @backstage/catalog-client@0.9.0-next.0 + - @backstage/plugin-auth-node@0.1.5-next.0 + - @backstage/plugin-azure-devops-backend@0.3.8-next.0 + - @backstage/plugin-code-coverage-backend@0.1.27-next.0 + - @backstage/plugin-graphql-backend@0.1.19-next.0 + - @backstage/plugin-jenkins-backend@0.1.18-next.0 + - @backstage/plugin-kafka-backend@0.2.22-next.0 + - @backstage/plugin-permission-backend@0.5.4-next.0 + - @backstage/plugin-permission-node@0.5.4-next.0 + - @backstage/plugin-proxy-backend@0.2.23-next.0 + - @backstage/plugin-tech-insights-backend@0.2.9-next.0 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.13-next.0 + - @backstage/plugin-tech-insights-node@0.2.7-next.0 + - @backstage/plugin-todo-backend@0.1.26-next.0 + - example-app@0.2.68-next.0 + +## 0.2.67 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/plugin-catalog-backend@0.23.0 + - @backstage/backend-common@0.12.0 + - @backstage/plugin-scaffolder-backend@0.17.3 + - @backstage/plugin-techdocs-backend@0.14.1 + - @backstage/plugin-auth-backend@0.12.0 + - @backstage/plugin-badges-backend@0.1.22 + - @backstage/plugin-code-coverage-backend@0.1.26 + - @backstage/plugin-jenkins-backend@0.1.17 + - @backstage/plugin-todo-backend@0.1.25 + - @backstage/integration@0.8.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-permission-node@0.5.3 + - @backstage/plugin-search-backend-node@0.5.0 + - @backstage/plugin-search-backend-module-pg@0.3.0 + - @backstage/plugin-search-backend-module-elasticsearch@0.1.0 + - @backstage/plugin-tech-insights-backend@0.2.8 + - example-app@0.2.67 + - @backstage/plugin-auth-node@0.1.4 + - @backstage/plugin-kafka-backend@0.2.21 + - @backstage/plugin-kubernetes-backend@0.4.11 + - @backstage/backend-tasks@0.1.10 + - @backstage/plugin-app-backend@0.3.28 + - @backstage/plugin-azure-devops-backend@0.3.7 + - @backstage/plugin-graphql-backend@0.1.18 + - @backstage/plugin-permission-backend@0.5.3 + - @backstage/plugin-proxy-backend@0.2.22 + - @backstage/plugin-rollbar-backend@0.1.25 + - @backstage/plugin-scaffolder-backend-module-rails@0.3.3 + - @backstage/plugin-search-backend@0.4.6 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.12 + - @backstage/plugin-tech-insights-node@0.2.6 + ## 0.2.66 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index cc960e3c24..50e7a9c3fb 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.66", + "version": "0.2.68-next.0", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,48 +18,48 @@ "backstage" ], "scripts": { - "build": "backstage-cli package build", - "build-image": "docker build ../.. -f Dockerfile --tag example-backend", "start": "backstage-cli package start", + "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "clean": "backstage-cli package clean", + "build-image": "docker build ../.. -f Dockerfile --tag example-backend", "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/backend-tasks": "^0.1.9", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/backend-tasks": "^0.2.0-next.0", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", - "@backstage/integration": "^0.7.5", - "@backstage/plugin-app-backend": "^0.3.27", - "@backstage/plugin-auth-backend": "^0.11.0", - "@backstage/plugin-auth-node": "^0.1.3", - "@backstage/plugin-azure-devops-backend": "^0.3.6", - "@backstage/plugin-badges-backend": "^0.1.21", - "@backstage/plugin-catalog-backend": "^0.22.0", - "@backstage/plugin-code-coverage-backend": "^0.1.25", - "@backstage/plugin-graphql-backend": "^0.1.17", - "@backstage/plugin-jenkins-backend": "^0.1.16", - "@backstage/plugin-kubernetes-backend": "^0.4.10", - "@backstage/plugin-kafka-backend": "^0.2.20", - "@backstage/plugin-permission-backend": "^0.5.2", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-permission-node": "^0.5.2", - "@backstage/plugin-proxy-backend": "^0.2.21", - "@backstage/plugin-rollbar-backend": "^0.1.24", - "@backstage/plugin-scaffolder-backend": "^0.17.0", - "@backstage/plugin-scaffolder-backend-module-rails": "^0.3.2", - "@backstage/plugin-search-backend": "^0.4.5", - "@backstage/plugin-search-backend-node": "^0.4.7", - "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.10", - "@backstage/plugin-search-backend-module-pg": "^0.2.9", - "@backstage/plugin-techdocs-backend": "^0.14.0", - "@backstage/plugin-tech-insights-backend": "^0.2.7", - "@backstage/plugin-tech-insights-node": "^0.2.5", - "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.11", - "@backstage/plugin-todo-backend": "^0.1.24", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-app-backend": "^0.3.29-next.0", + "@backstage/plugin-auth-backend": "^0.12.1-next.0", + "@backstage/plugin-auth-node": "^0.1.5-next.0", + "@backstage/plugin-azure-devops-backend": "^0.3.8-next.0", + "@backstage/plugin-badges-backend": "^0.1.23-next.0", + "@backstage/plugin-catalog-backend": "^0.24.0-next.0", + "@backstage/plugin-code-coverage-backend": "^0.1.27-next.0", + "@backstage/plugin-graphql-backend": "^0.1.19-next.0", + "@backstage/plugin-jenkins-backend": "^0.1.18-next.0", + "@backstage/plugin-kubernetes-backend": "^0.4.12-next.0", + "@backstage/plugin-kafka-backend": "^0.2.22-next.0", + "@backstage/plugin-permission-backend": "^0.5.4-next.0", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-permission-node": "^0.5.4-next.0", + "@backstage/plugin-proxy-backend": "^0.2.23-next.0", + "@backstage/plugin-rollbar-backend": "^0.1.26-next.0", + "@backstage/plugin-scaffolder-backend": "^0.18.0-next.0", + "@backstage/plugin-scaffolder-backend-module-rails": "^0.3.4-next.0", + "@backstage/plugin-search-backend": "^0.4.7-next.0", + "@backstage/plugin-search-backend-node": "^0.5.1-next.0", + "@backstage/plugin-search-backend-module-elasticsearch": "^0.1.1-next.0", + "@backstage/plugin-search-backend-module-pg": "^0.3.1-next.0", + "@backstage/plugin-techdocs-backend": "^0.14.2-next.0", + "@backstage/plugin-tech-insights-backend": "^0.2.9-next.0", + "@backstage/plugin-tech-insights-node": "^0.2.7-next.0", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.13-next.0", + "@backstage/plugin-todo-backend": "^0.1.26-next.0", "@gitbeaker/node": "^35.1.0", "@octokit/rest": "^18.5.3", "@vscode/sqlite3": "^5.0.7", @@ -76,7 +76,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/dockerode": "^3.3.0", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 619134a990..e0f3a4b1ca 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import { DockerContainerRunner } from '@backstage/backend-common'; import { CatalogClient } from '@backstage/catalog-client'; import { createRouter } from '@backstage/plugin-scaffolder-backend'; -import Docker from 'dockerode'; import { Router } from 'express'; import type { PluginEnvironment } from '../types'; @@ -28,13 +26,8 @@ export default async function createPlugin({ reader, discovery, }: PluginEnvironment): Promise { - const dockerClient = new Docker(); - const containerRunner = new DockerContainerRunner({ dockerClient }); - const catalogClient = new CatalogClient({ discoveryApi: discovery }); - return await createRouter({ - containerRunner, logger, config, database, diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index 4b5363e73d..a90b47cd81 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -18,7 +18,7 @@ import { useHotCleanup, } from '@backstage/backend-common'; import { Config } from '@backstage/config'; -import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; +import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; import { createRouter } from '@backstage/plugin-search-backend'; import { ElasticSearchSearchEngine } from '@backstage/plugin-search-backend-module-elasticsearch'; import { PgSearchEngine } from '@backstage/plugin-search-backend-module-pg'; @@ -27,7 +27,7 @@ import { LunrSearchEngine, SearchEngine, } from '@backstage/plugin-search-backend-node'; -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; import { Logger } from 'winston'; import { PluginEnvironment } from '../types'; @@ -70,7 +70,7 @@ export default async function createPlugin({ // particular collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultCatalogCollator.fromConfig(config, { + factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery, tokenManager, }), @@ -78,7 +78,7 @@ export default async function createPlugin({ indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultTechDocsCollator.fromConfig(config, { + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { discovery, logger, tokenManager, diff --git a/packages/catalog-client/.eslintrc.js b/packages/catalog-client/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/catalog-client/.eslintrc.js +++ b/packages/catalog-client/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 93f248ff0e..f3b74c4d4d 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/catalog-client +## 0.9.0-next.0 + +### Minor Changes + +- bf95bb806c: **BREAKING**: Removed previously deprecated `CatalogApi.getEntityByName`, please use `getEntityByRef` instead. +- a3eb3d2afa: **BREAKING**: Removed `CatalogClient.getLocationByEntity` and `CatalogClient.getOriginLocationByEntity` which had previously been deprecated. Please use `CatalogApi.getLocationByRef` instead. Note that this only affects you if you were using `CatalogClient` (the class) directly, rather than `CatalogApi` (the interface), since it has been removed from the interface in an earlier release. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.13.0-next.0 + +## 0.8.0 + +### Minor Changes + +- bb2ba5f10d: **BREAKING**: Removed the old deprecated request/response types: + + - `CatalogEntitiesRequest` - please use `GetEntitiesRequest` instead + - `CatalogEntityAncestorsRequest` - please use `GetEntityAncestorsRequest` instead + - `CatalogEntityAncestorsResponse` - please use `GetEntityAncestorsResponse` instead + - `CatalogListResponse` - please use `GetEntitiesResponse` instead + +### Patch Changes + +- a52f69987a: **DEPRECATION**: Deprecated `getEntityByName` from `CatalogApi` and added `getEntityByRef` instead, which accepts both string and compound ref forms. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + ## 0.7.2 ### Patch Changes diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index c957859fec..ac8f712e11 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -3,8 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; // @public export type AddLocationRequest = { @@ -38,8 +38,8 @@ export interface CatalogApi { request: GetEntityAncestorsRequest, options?: CatalogRequestOptions, ): Promise; - getEntityByName( - name: EntityName, + getEntityByRef( + entityRef: string | CompoundEntityRef, options?: CatalogRequestOptions, ): Promise; getEntityFacets( @@ -90,19 +90,19 @@ export class CatalogClient implements CatalogApi { request: GetEntityAncestorsRequest, options?: CatalogRequestOptions, ): Promise; + // @deprecated (undocumented) getEntityByName( - compoundName: EntityName, + compoundName: CompoundEntityRef, + options?: CatalogRequestOptions, + ): Promise; + getEntityByRef( + entityRef: string | CompoundEntityRef, options?: CatalogRequestOptions, ): Promise; getEntityFacets( request: GetEntityFacetsRequest, options?: CatalogRequestOptions, ): Promise; - // @deprecated (undocumented) - getLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise; getLocationById( id: string, options?: CatalogRequestOptions, @@ -111,11 +111,6 @@ export class CatalogClient implements CatalogApi { locationRef: string, options?: CatalogRequestOptions, ): Promise; - // @deprecated (undocumented) - getOriginLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise; refreshEntity( entityRef: string, options?: CatalogRequestOptions, @@ -130,18 +125,6 @@ export class CatalogClient implements CatalogApi { ): Promise; } -// @public @deprecated (undocumented) -export type CatalogEntitiesRequest = GetEntitiesRequest; - -// @public @deprecated (undocumented) -export type CatalogEntityAncestorsRequest = GetEntityAncestorsRequest; - -// @public @deprecated (undocumented) -export type CatalogEntityAncestorsResponse = GetEntityAncestorsResponse; - -// @public @deprecated (undocumented) -export type CatalogListResponse<_Entity> = GetEntitiesResponse; - // @public export interface CatalogRequestOptions { // (undocumented) diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index ffcfbc16b7..cf9246d5a2 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-client", "description": "An isomorphic client for the catalog backend", - "version": "0.7.2", + "version": "0.9.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,12 +33,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/errors": "^0.2.2", "cross-fetch": "^3.1.5" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/jest": "^26.0.7", "msw": "^0.35.0" }, diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 0035691f2c..0b838eb4c2 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -195,6 +195,60 @@ describe('CatalogClient', () => { }); }); + describe('getEntityByRef', () => { + const existingEntity: Entity = { + apiVersion: 'v1', + kind: 'CustomKind', + metadata: { + namespace: 'default', + name: 'exists', + }, + }; + + beforeEach(() => { + server.use( + rest.get( + `${mockBaseUrl}/entities/by-name/customkind/default/exists`, + (_, res, ctx) => { + return res(ctx.json(existingEntity)); + }, + ), + rest.get( + `${mockBaseUrl}/entities/by-name/customkind/default/missing`, + (_, res, ctx) => { + return res(ctx.status(404)); + }, + ), + ); + }); + + it('finds by string and compound', async () => { + await expect( + client.getEntityByRef('customkind:default/exists'), + ).resolves.toEqual(existingEntity); + await expect( + client.getEntityByRef({ + kind: 'CustomKind', + namespace: 'default', + name: 'exists', + }), + ).resolves.toEqual(existingEntity); + }); + + it('returns undefined for 404s', async () => { + await expect( + client.getEntityByRef('customkind:default/missing'), + ).resolves.toBeUndefined(); + await expect( + client.getEntityByRef({ + kind: 'CustomKind', + namespace: 'default', + name: 'missing', + }), + ).resolves.toBeUndefined(); + }); + }); + describe('getLocationById', () => { const defaultResponse = { data: { diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index bb3c9604b3..85aada616c 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -15,10 +15,8 @@ */ import { - ANNOTATION_LOCATION, - ANNOTATION_ORIGIN_LOCATION, Entity, - EntityName, + CompoundEntityRef, parseEntityRef, stringifyEntityRef, stringifyLocationRef, @@ -171,10 +169,31 @@ export class CatalogClient implements CatalogApi { } /** - * {@inheritdoc CatalogApi.getEntityByName} + * {@inheritdoc CatalogApi.getEntityByRef} + */ + async getEntityByRef( + entityRef: string | CompoundEntityRef, + options?: CatalogRequestOptions, + ): Promise { + const { kind, namespace, name } = parseEntityRef(entityRef); + return this.requestOptional( + 'GET', + `/entities/by-name/${encodeURIComponent(kind)}/${encodeURIComponent( + namespace, + )}/${encodeURIComponent(name)}`, + options, + ); + } + + // NOTE(freben): When we deprecate getEntityByName from the interface, we may + // still want to leave this implementation in place for quite some time + // longer, to minimize the risk for breakages. Suggested date for removal: + // August 2022 + /** + * @deprecated Use getEntityByRef instead */ async getEntityByName( - compoundName: EntityName, + compoundName: CompoundEntityRef, options?: CatalogRequestOptions, ): Promise { const { kind, namespace = 'default', name } = compoundName; @@ -287,49 +306,6 @@ export class CatalogClient implements CatalogApi { }; } - /** - * @deprecated please use getLocationByRef instead - */ - async getOriginLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise { - const locationCompound = - entity.metadata.annotations?.[ANNOTATION_ORIGIN_LOCATION]; - if (!locationCompound) { - return undefined; - } - const all: { data: Location }[] = await this.requestRequired( - 'GET', - '/locations', - options, - ); - return all - .map(r => r.data) - .find(l => locationCompound === stringifyLocationRef(l)); - } - - /** - * @deprecated please use getLocationByRef instead - */ - async getLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise { - const locationCompound = entity.metadata.annotations?.[ANNOTATION_LOCATION]; - if (!locationCompound) { - return undefined; - } - const all: { data: Location }[] = await this.requestRequired( - 'GET', - '/locations', - options, - ); - return all - .map(r => r.data) - .find(l => locationCompound === stringifyLocationRef(l)); - } - /** * {@inheritdoc CatalogApi.getLocationByRef} */ diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index 4a86ca41f3..ba0c5e9359 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; /** * This symbol can be used in place of a value when passed to filters in e.g. @@ -306,11 +306,12 @@ export interface CatalogApi { * Gets a single entity from the catalog by its ref (kind, namespace, name) * triplet. * - * @param name - A complete entity ref + * @param entityRef - A complete entity ref, either on string or compound form * @param options - Additional options + * @returns The matching entity, or undefined if there was no entity with that ref */ - getEntityByName( - name: EntityName, + getEntityByRef( + entityRef: string | CompoundEntityRef, options?: CatalogRequestOptions, ): Promise; diff --git a/packages/catalog-client/src/types/index.ts b/packages/catalog-client/src/types/index.ts index 39c8962b68..8bf4e34da2 100644 --- a/packages/catalog-client/src/types/index.ts +++ b/packages/catalog-client/src/types/index.ts @@ -28,5 +28,4 @@ export type { GetEntityFacetsRequest, GetEntityFacetsResponse, } from './api'; -export * from './deprecated'; export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status'; diff --git a/packages/catalog-model/.eslintrc.js b/packages/catalog-model/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/catalog-model/.eslintrc.js +++ b/packages/catalog-model/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index cbb2e681bf..8a35937c07 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/catalog-model +## 0.13.0-next.0 + +### Minor Changes + +- 51a9f8f122: **BREAKING**: + + - Removed the previously deprecated type `EntityRef`. Please use `string` for stringified entity refs, `CompoundEntityRef` for compound kind-namespace-name triplet objects, or custom objects like `{ kind?: string; namespace?: string; name: string }` and similar if you have need for partial types. + - Removed the previously deprecated type `LocationSpec` type, which has been moved to `@backstage/plugin-catalog-backend`. + - Removed the previously deprecated function `parseEntityName`. Please use `parseEntityRef` instead. + +- d1d488e371: **BREAKING**: The default validator for `metadata.tags` now permits the colon (`:`) character as well. + +### Patch Changes + +- 2952566587: Updated `parseEntityRef` to allow `:` and `/` in the entity name. For example, parsing `'component:default/foo:bar'` will result in the name `'foo:bar'`. + + Note that only parsing `'foo:bar'` itself will result in the name `'bar'` and the entity kind `'foo'`, meaning this is a particularly nasty trap for user defined entity references. For this reason it is strongly discouraged to use names that contain these characters, and the catalog model does not allow it by default. However, this change now makes is possible to use these names if the default catalog validation is replaced, and in particular a high level of automation of the catalog population can limit issues that it might otherwise cause. + +- b1aacbf96a: Applied the fix for the `/alpha` entry point resolution that was part of the `v0.70.1` release of Backstage. +- d1d488e371: **DEPRECATION**: + + - Deprecated `CommonValidatorFunctions.isValidString`, please use `isNonEmptyString` instead which is equivalent but better named. + - Deprecated `CommonValidatorFunctions.isValidTag`, with no replacement. Its purpose was too specific and not reusable, so it will be removed. + +## 0.12.1 + +### Patch Changes + +- Fixed runtime resolution of the `/alpha` entry point. + +## 0.12.0 + +### Minor Changes + +- ac7b1161a6: **BREAKING**: The following changes are all breaking changes. + + Removed `EDIT_URL_ANNOTATION` and `VIEW_URL_ANNOTATION`, `LOCATION_ANNOTATION`, `ORIGIN_LOCATION_ANNOTATION`, `LOCATION_ANNOTATION`, `SOURCE_LOCATION_ANNOTATION`. All of these constants have been prefixed with ANNOTATION to be easier to find meaning `SOURCE_LOCATION_ANNOTATION` is available as `ANNOTATION_SOURCE_LOCATION`. + + Removed `parseLocationReference`, replaced by `parseLocationRef`. + + Removed `stringifyLocationReference`, replaced by `stringifyLocationRef`. + + Removed `Location` type which has been moved to `catalog-client`. + + Removed `ENTITY_DEFAULT_NAMESPACE`, replaced by `DEFAULT_NAMESPACE`. + + Removed `compareEntityToRef` compare using `stringifyEntityRef` instead. + + Removed `JSONSchema` type which should be imported from `json-schema` package instead. + + Removed utility methods: `entityHasChanges`, `generateEntityEtag`, `generateEntityUid`, `generateUpdatedEntity`. + + Removed `ENTITY_META_GENERATED_FIELDS` and `EntityRefContext`. + +### Patch Changes + +- debfcd9515: Move `@types/json-schema` to be a dev dependency +- 36aa63022b: **DEPRECATION**: Deprecated the `EntityName` type, and added the better-named `CompoundEntityRef` to replace it. + + **DEPRECATION**: Deprecated the `getEntityName` function, and added the better-named `getCompoundEntityRef` to replace it. + + Please switch over to using the new symbols, as the old ones may be removed in a future release. + ## 0.11.0 ### Minor Changes diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 210d0187e2..90242f9293 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -4,8 +4,6 @@ ```ts import { JsonObject } from '@backstage/types'; -import { JSONSchema7 } from 'json-schema'; -import { JsonValue } from '@backstage/types'; import { SerializedError } from '@backstage/errors'; // @alpha @@ -53,6 +51,7 @@ export const apiEntityV1alpha1Validator: KindValidator; // @public export class CommonValidatorFunctions { static isJsonSafe(value: unknown): boolean; + static isNonEmptyString(value: unknown): value is string; static isValidDnsLabel(value: unknown): boolean; static isValidDnsSubdomain(value: unknown): boolean; static isValidPrefixAndOrSuffix( @@ -61,28 +60,13 @@ export class CommonValidatorFunctions { isValidPrefix: (value: string) => boolean, isValidSuffix: (value: string) => boolean, ): boolean; + // @deprecated static isValidString(value: unknown): boolean; + // @deprecated static isValidTag(value: unknown): boolean; static isValidUrl(value: unknown): boolean; } -// @public @deprecated -export function compareEntityToRef( - entity: Entity, - ref: - | string - | { - kind?: string; - namespace?: string; - name: string; - } - | EntityName, - context?: { - defaultKind?: string; - defaultNamespace?: string; - }, -): boolean; - // @public interface ComponentEntityV1alpha1 extends Entity { // (undocumented) @@ -107,6 +91,13 @@ export { ComponentEntityV1alpha1 }; // @public export const componentEntityV1alpha1Validator: KindValidator; +// @public +export type CompoundEntityRef = { + kind: string; + namespace: string; + name: string; +}; + // @public export const DEFAULT_NAMESPACE = 'default'; @@ -134,9 +125,6 @@ export { DomainEntityV1alpha1 }; // @public export const domainEntityV1alpha1Validator: KindValidator; -// @public @deprecated -export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; - // @public export type Entity = { apiVersion: string; @@ -146,16 +134,6 @@ export type Entity = { relations?: EntityRelation[]; }; -// @public @deprecated -export const ENTITY_DEFAULT_NAMESPACE = 'default'; - -// @public @deprecated -export const ENTITY_META_GENERATED_FIELDS: readonly [ - 'uid', - 'etag', - 'generation', -]; - // @public export type EntityEnvelope = { apiVersion: string; @@ -171,9 +149,6 @@ export function entityEnvelopeSchemaValidator< T extends EntityEnvelope = EntityEnvelope, >(schema?: unknown): (data: unknown) => T; -// @public @deprecated -export function entityHasChanges(previous: Entity, next: Entity): boolean; - // @public export function entityKindSchemaValidator( schema: unknown, @@ -201,12 +176,8 @@ export type EntityMeta = JsonObject & { links?: EntityLink[]; }; -// @public -export type EntityName = { - kind: string; - namespace: string; - name: string; -}; +// @public @deprecated +export type EntityName = CompoundEntityRef; // @public export const EntityPolicies: { @@ -219,25 +190,10 @@ export type EntityPolicy = { enforce(entity: Entity): Promise; }; -// @public @deprecated -export type EntityRef = - | string - | { - kind?: string; - namespace?: string; - name: string; - }; - -// @public @deprecated -export type EntityRefContext = { - defaultKind?: string; - defaultNamespace?: string; -}; - // @public export type EntityRelation = { type: string; - target: EntityName; + target: CompoundEntityRef; targetRef: string; }; @@ -269,17 +225,11 @@ export class FieldFormatEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @public @deprecated -export function generateEntityEtag(): string; - -// @public @deprecated -export function generateEntityUid(): string; - -// @public @deprecated -export function generateUpdatedEntity(previous: Entity, next: Entity): Entity; - // @public -export function getEntityName(entity: Entity): EntityName; +export function getCompoundEntityRef(entity: Entity): CompoundEntityRef; + +// @public @deprecated +export const getEntityName: typeof getCompoundEntityRef; // @public export function getEntitySourceLocation(entity: Entity): { @@ -312,11 +262,6 @@ export { GroupEntityV1alpha1 }; // @public export const groupEntityV1alpha1Validator: KindValidator; -// @public @deprecated -export type JSONSchema = JSONSchema7 & { - [key in string]?: JsonValue; -}; - // @public export type KindValidator = { check(entity: Entity): Promise; @@ -342,15 +287,6 @@ export class KubernetesValidatorFunctions { static isValidObjectName(value: unknown): boolean; } -// @public @deprecated -type Location_2 = { - id: string; -} & LocationSpec; -export { Location_2 as Location }; - -// @public @deprecated -export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; - // @public interface LocationEntityV1alpha1 extends Entity { // (undocumented) @@ -371,13 +307,6 @@ export { LocationEntityV1alpha1 }; // @public export const locationEntityV1alpha1Validator: KindValidator; -// @public @deprecated -export type LocationSpec = { - type: string; - target: string; - presence?: 'optional' | 'required'; -}; - // @public export function makeValidator(overrides?: Partial): Validators; @@ -388,25 +317,6 @@ export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @public @deprecated -export const ORIGIN_LOCATION_ANNOTATION = - 'backstage.io/managed-by-origin-location'; - -// @public @deprecated -export function parseEntityName( - ref: - | string - | { - kind?: string; - namespace?: string; - name: string; - }, - context?: { - defaultKind?: string; - defaultNamespace?: string; - }, -): EntityName; - // @public export function parseEntityRef( ref: @@ -420,7 +330,7 @@ export function parseEntityRef( defaultKind?: string; defaultNamespace?: string; }, -): EntityName; +): CompoundEntityRef; // @public export function parseLocationRef(ref: string): { @@ -428,12 +338,6 @@ export function parseLocationRef(ref: string): { target: string; }; -// @public @deprecated -export function parseLocationReference(ref: string): { - type: string; - target: string; -}; - // @public export const RELATION_API_CONSUMED_BY = 'apiConsumedBy'; @@ -503,9 +407,6 @@ export class SchemaValidEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @public @deprecated -export const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location'; - // @public export function stringifyEntityRef( ref: @@ -523,12 +424,6 @@ export function stringifyLocationRef(ref: { target: string; }): string; -// @public @deprecated -export function stringifyLocationReference(ref: { - type: string; - target: string; -}): string; - // @public interface SystemEntityV1alpha1 extends Entity { // (undocumented) @@ -581,7 +476,4 @@ export type Validators = { isValidAnnotationValue(value: unknown): boolean; isValidTag(value: unknown): boolean; }; - -// @public @deprecated -export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; ``` diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 70ed0fbe4d..bd0e88acbf 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-model", "description": "Types and validators that help describe the model of a Backstage Catalog", - "version": "0.11.0", + "version": "0.13.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,15 +37,15 @@ "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@backstage/types": "^0.1.3", - "@types/json-schema": "^7.0.5", "ajv": "^7.0.3", "json-schema": "^0.4.0", "lodash": "^4.17.21", "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/jest": "^26.0.7", + "@types/json-schema": "^7.0.5", "@types/lodash": "^4.14.151", "yaml": "^1.9.2" }, diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index ef9b4352da..029d9e095b 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -15,7 +15,7 @@ */ import { JsonObject } from '@backstage/types'; -import { EntityName } from '../types'; +import { CompoundEntityRef } from '../types'; import { EntityStatus } from './EntityStatus'; /** @@ -201,7 +201,7 @@ export type EntityRelation = { * * @deprecated use targetRef instead */ - target: EntityName; + target: CompoundEntityRef; /** * The entity ref of the target of this relation. diff --git a/packages/catalog-model/src/entity/constants.ts b/packages/catalog-model/src/entity/constants.ts index 6cef19cac9..5dc21683bd 100644 --- a/packages/catalog-model/src/entity/constants.ts +++ b/packages/catalog-model/src/entity/constants.ts @@ -14,14 +14,6 @@ * limitations under the License. */ -/** - * The namespace that entities without an explicit namespace fall into. - * - * @public - * @deprecated use {@link DEFAULT_NAMESPACE} instead. - */ -export const ENTITY_DEFAULT_NAMESPACE = 'default'; - /** * The namespace that entities without an explicit namespace fall into. * @@ -29,34 +21,6 @@ export const ENTITY_DEFAULT_NAMESPACE = 'default'; */ export const DEFAULT_NAMESPACE = 'default'; -/** - * The keys of EntityMeta that are auto-generated. - * - * @public - * @deprecated will be removed in a future release. - */ -export const ENTITY_META_GENERATED_FIELDS = [ - 'uid', - 'etag', - 'generation', -] as const; - -/** - * Annotation for linking to entity page from catalog pages. - * - * @public - * @deprecated use {@link ANNOTATION_VIEW_URL} instead. - */ -export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; - -/** - * Annotation for linking to entity edit page from catalog pages. - * - * @public - * @deprecated use {@link ANNOTATION_EDIT_URL} instead. - */ -export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; - /** * Annotation for linking to entity page from catalog pages. * diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index f619d0f39e..f46f53945f 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -15,11 +15,7 @@ */ export { - EDIT_URL_ANNOTATION, - ENTITY_DEFAULT_NAMESPACE, DEFAULT_NAMESPACE, - ENTITY_META_GENERATED_FIELDS, - VIEW_URL_ANNOTATION, ANNOTATION_EDIT_URL, ANNOTATION_VIEW_URL, } from './constants'; @@ -38,16 +34,8 @@ export type { } from './EntityStatus'; export * from './policies'; export { - compareEntityToRef, + getCompoundEntityRef, getEntityName, - parseEntityName, parseEntityRef, stringifyEntityRef, } from './ref'; -export type { EntityRefContext } from './ref'; -export { - entityHasChanges, - generateEntityEtag, - generateEntityUid, - generateUpdatedEntity, -} from './util'; diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 51059328a6..22d0e5e997 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -95,6 +95,7 @@ export class FieldFormatEntityPolicy implements EntityPolicy { expectation = 'a string that is a valid url'; break; case 'isValidString': + case 'isNonEmptyString': expectation = 'a non empty string'; break; default: @@ -156,7 +157,7 @@ export class FieldFormatEntityPolicy implements EntityPolicy { optional( `links.${i}.title`, links[i]?.title, - CommonValidatorFunctions.isValidString, + CommonValidatorFunctions.isNonEmptyString, ); optional( `links.${i}.icon`, diff --git a/packages/catalog-model/src/entity/ref.test.ts b/packages/catalog-model/src/entity/ref.test.ts index b0c7068fae..0945eddf6e 100644 --- a/packages/catalog-model/src/entity/ref.test.ts +++ b/packages/catalog-model/src/entity/ref.test.ts @@ -14,172 +14,9 @@ * limitations under the License. */ -import { DEFAULT_NAMESPACE } from './constants'; -import { Entity } from './Entity'; -import { compareEntityToRef, parseEntityName, parseEntityRef } from './ref'; +import { parseEntityRef } from './ref'; describe('ref', () => { - describe('parseEntityName', () => { - it('handles some omissions', () => { - expect(parseEntityName('a:b/c')).toEqual({ - kind: 'a', - namespace: 'b', - name: 'c', - }); - expect(() => parseEntityName('b/c')).toThrow(/kind/); - expect(parseEntityName('a:c')).toEqual({ - kind: 'a', - namespace: DEFAULT_NAMESPACE, - name: 'c', - }); - expect(() => parseEntityName('c')).toThrow(/kind/); - }); - - it('rejects bad inputs', () => { - expect(() => parseEntityName(null as any)).toThrow(); - expect(() => parseEntityName(7 as any)).toThrow(); - expect(() => parseEntityName('a:b:c')).toThrow(); - expect(() => parseEntityName('a/b/c')).toThrow(); - expect(() => parseEntityName('a/b:c')).toThrow(); - expect(() => parseEntityName('a:b/c/d')).toThrow(); - expect(() => parseEntityName('a:b/c:d')).toThrow(); - }); - - it('rejects empty parts in strings', () => { - // one is empty - expect(() => parseEntityName(':b/c')).toThrow(); - expect(() => parseEntityName('a:/c')).toThrow(); - expect(() => parseEntityName('a:b/')).toThrow(); - // two are empty - expect(() => parseEntityName('a:/')).toThrow(); - expect(() => parseEntityName(':b/')).toThrow(); - expect(() => parseEntityName(':/c')).toThrow(); - // three are empty - expect(() => parseEntityName(':/')).toThrow(); - // one is left out, one empty - expect(() => parseEntityName('/c')).toThrow(); - expect(() => parseEntityName('b/')).toThrow(); - expect(() => parseEntityName(':c')).toThrow(); - expect(() => parseEntityName('a:')).toThrow(); - // nothing at all - expect(() => parseEntityName('')).toThrow(); - }); - - it('rejects empty parts in compounds', () => { - // one is empty - expect(() => - parseEntityName({ kind: '', namespace: 'b', name: 'c' }), - ).toThrow(); - expect(() => parseEntityName({ namespace: 'b', name: 'c' })).toThrow(); - expect(() => - parseEntityName({ kind: 'a', namespace: '', name: 'c' }), - ).toThrow(); - expect(() => parseEntityName({ kind: 'a', name: 'c' })).not.toThrow(); - expect(() => - parseEntityName({ kind: 'a', namespace: 'b', name: '' }), - ).toThrow(); - expect(() => - parseEntityName({ kind: 'a', namespace: 'b' } as any), - ).toThrow(); - // two are empty - expect(() => - parseEntityName({ kind: '', namespace: '', name: 'c' }), - ).toThrow(); - expect(() => parseEntityName({ name: 'c' })).toThrow(); - expect(() => - parseEntityName({ kind: '', namespace: 'b', name: '' }), - ).toThrow(); - expect(() => parseEntityName({ namespace: 'b' } as any)).toThrow(); - expect(() => - parseEntityName({ kind: 'a', namespace: '', name: '' }), - ).toThrow(); - expect(() => parseEntityName({ kind: 'a' } as any)).toThrow(); - // three are empty - expect(() => - parseEntityName({ kind: '', namespace: '', name: '' }), - ).toThrow(); - expect(() => parseEntityName({} as any)).toThrow(); - // one is left out, one empty - expect(() => parseEntityName({ namespace: '', name: 'c' })).toThrow(); - expect(() => parseEntityName({ namespace: 'b', name: '' })).toThrow(); - expect(() => parseEntityName({ kind: '', name: 'c' })).toThrow(); - expect(() => parseEntityName({ kind: 'a', name: '' })).toThrow(); - }); - - it('adds defaults where necessary to strings', () => { - expect( - parseEntityName('a:b/c', { defaultKind: 'x', defaultNamespace: 'y' }), - ).toEqual({ kind: 'a', namespace: 'b', name: 'c' }); - expect( - parseEntityName('b/c', { defaultKind: 'x', defaultNamespace: 'y' }), - ).toEqual({ kind: 'x', namespace: 'b', name: 'c' }); - expect( - parseEntityName('a:c', { defaultKind: 'x', defaultNamespace: 'y' }), - ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); - expect(parseEntityName('a:c', { defaultKind: 'x' })).toEqual({ - kind: 'a', - namespace: DEFAULT_NAMESPACE, - name: 'c', - }); - expect( - parseEntityName('c', { defaultKind: 'x', defaultNamespace: 'y' }), - ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); - expect(parseEntityName('c', { defaultKind: 'x' })).toEqual({ - kind: 'x', - namespace: DEFAULT_NAMESPACE, - name: 'c', - }); - }); - - it('adds defaults where necessary to compounds', () => { - expect( - parseEntityName( - { kind: 'a', namespace: 'b', name: 'c' }, - { defaultKind: 'x', defaultNamespace: 'y' }, - ), - ).toEqual({ kind: 'a', namespace: 'b', name: 'c' }); - expect( - parseEntityName( - { namespace: 'b', name: 'c' }, - { defaultKind: 'x', defaultNamespace: 'y' }, - ), - ).toEqual({ kind: 'x', namespace: 'b', name: 'c' }); - expect( - parseEntityName( - { kind: 'a', name: 'c' }, - { defaultKind: 'x', defaultNamespace: 'y' }, - ), - ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); - expect( - parseEntityName({ kind: 'a', name: 'c' }, { defaultKind: 'x' }), - ).toEqual({ kind: 'a', namespace: DEFAULT_NAMESPACE, name: 'c' }); - expect( - parseEntityName( - { name: 'c' }, - { defaultKind: 'x', defaultNamespace: 'y' }, - ), - ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); - expect(parseEntityName({ name: 'c' }, { defaultKind: 'x' })).toEqual({ - kind: 'x', - namespace: DEFAULT_NAMESPACE, - name: 'c', - }); - // empty strings are errors, not defaults - expect(() => - parseEntityName( - { kind: '', namespace: 'b', name: 'c' }, - { defaultKind: 'x', defaultNamespace: 'y' }, - ), - ).toThrow(/kind/); - expect(() => - parseEntityName( - { kind: 'a', namespace: '', name: 'c' }, - { defaultKind: 'x', defaultNamespace: 'y' }, - ), - ).toThrow(/namespace/); - }); - }); - describe('parseEntityRef', () => { it('handles some omissions', () => { expect(parseEntityRef('a:b/c')).toEqual({ @@ -197,11 +34,24 @@ describe('ref', () => { it('rejects bad inputs', () => { expect(() => parseEntityRef(null as any)).toThrow(); expect(() => parseEntityRef(7 as any)).toThrow(); - expect(() => parseEntityRef('a:b:c')).toThrow(); - expect(() => parseEntityRef('a/b/c')).toThrow(); - expect(() => parseEntityRef('a/b:c')).toThrow(); - expect(() => parseEntityRef('a:b/c/d')).toThrow(); - expect(() => parseEntityRef('a:b/c:d')).toThrow(); + }); + + it('allows names with : and /', () => { + expect( + parseEntityRef('a:b:c', { defaultKind: 'k', defaultNamespace: 'ns' }), + ).toEqual({ kind: 'a', namespace: 'ns', name: 'b:c' }); + expect( + parseEntityRef('a/b/c', { defaultKind: 'k', defaultNamespace: 'ns' }), + ).toEqual({ kind: 'k', namespace: 'a', name: 'b/c' }); + expect( + parseEntityRef('a/b:c', { defaultKind: 'k', defaultNamespace: 'ns' }), + ).toEqual({ kind: 'k', namespace: 'a', name: 'b:c' }); + expect( + parseEntityRef('a:b/c/d', { defaultKind: 'k', defaultNamespace: 'ns' }), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c/d' }); + expect( + parseEntityRef('a:b/c:d', { defaultKind: 'k', defaultNamespace: 'ns' }), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c:d' }); }); it('rejects empty parts in strings', () => { @@ -313,320 +163,4 @@ describe('ref', () => { ).toThrow(/namespace/); }); }); - - describe('compareEntityToRef', () => { - const entityWithNamespace: Entity = { - apiVersion: 'a', - kind: 'K', - metadata: { - name: 'n', - namespace: 'ns', - }, - }; - const entityWithoutNamespace: Entity = { - apiVersion: 'a', - kind: 'K', - metadata: { - name: 'n', - }, - }; - - it('handles matching string refs', () => { - expect(compareEntityToRef(entityWithNamespace, 'K:ns/n')).toBe(true); - expect(compareEntityToRef(entityWithNamespace, 'k:nS/N')).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'K:n', { - defaultNamespace: 'ns', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'K:n', { - defaultNamespace: 'Ns', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'ns/n', { defaultKind: 'K' }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'n', { - defaultKind: 'K', - defaultNamespace: 'ns', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'N', { - defaultKind: 'k', - defaultNamespace: 'nS', - }), - ).toBe(true); - - expect(compareEntityToRef(entityWithoutNamespace, 'K:default/n')).toBe( - true, - ); - expect(compareEntityToRef(entityWithoutNamespace, 'K:deFault/n')).toBe( - true, - ); - expect( - compareEntityToRef(entityWithoutNamespace, 'K:n', { - defaultNamespace: 'default', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, 'K:n', { - defaultNamespace: 'deFault', - }), - ).toBe(true); - expect(compareEntityToRef(entityWithoutNamespace, 'K:default/n')).toBe( - true, - ); - expect(compareEntityToRef(entityWithoutNamespace, 'K:n')).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, 'default/n', { - defaultKind: 'K', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, 'n', { - defaultKind: 'K', - defaultNamespace: 'default', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, 'n', { - defaultKind: 'K', - }), - ).toBe(true); - }); - - it('handles mismatching string refs', () => { - expect(compareEntityToRef(entityWithNamespace, 'X:ns/n')).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, 'ns/n', { - defaultKind: 'X', - }), - ).toBe(false); - - expect(compareEntityToRef(entityWithNamespace, 'K:xx/n')).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, 'K:n', { - defaultNamespace: 'xx', - }), - ).toBe(false); - - expect(compareEntityToRef(entityWithNamespace, 'K:ns/x')).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, 'x', { - defaultKind: 'K', - defaultNamespace: 'ns', - }), - ).toBe(false); - }); - - it('handles matching compound refs', () => { - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'K', - namespace: 'ns', - name: 'n', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'k', - namespace: 'Ns', - name: 'N', - }), - ).toBe(true); - expect( - compareEntityToRef( - entityWithNamespace, - { kind: 'K', name: 'n' }, - { - defaultNamespace: 'ns', - }, - ), - ).toBe(true); - expect( - compareEntityToRef( - entityWithNamespace, - { namespace: 'ns', name: 'n' }, - { defaultKind: 'K' }, - ), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'n', { - defaultKind: 'K', - defaultNamespace: 'ns', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'N', { - defaultKind: 'k', - defaultNamespace: 'nS', - }), - ).toBe(true); - - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'K', - namespace: 'default', - name: 'n', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'k', - namespace: 'deFault', - name: 'N', - }), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { kind: 'K', name: 'n' }, - { - defaultNamespace: 'default', - }, - ), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, { kind: 'K', name: 'n' }), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { namespace: 'default', name: 'n' }, - { - defaultKind: 'K', - }, - ), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { name: 'n' }, - { - defaultKind: 'K', - defaultNamespace: 'default', - }, - ), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { name: 'N' }, - { - defaultKind: 'k', - defaultNamespace: 'defAult', - }, - ), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { name: 'n' }, - { - defaultKind: 'K', - }, - ), - ).toBe(true); - }); - - it('handles mismatching compound refs', () => { - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'X', - namespace: 'ns', - name: 'n', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithNamespace, - { - namespace: 'ns', - name: 'n', - }, - { defaultKind: 'X' }, - ), - ).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'X', - namespace: 'default', - name: 'n', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithoutNamespace, - { - namespace: 'default', - name: 'n', - }, - { defaultKind: 'X' }, - ), - ).toBe(false); - - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'K', - namespace: 'xx', - name: 'n', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithNamespace, - { - kind: 'K', - name: 'n', - }, - { defaultNamespace: 'xx' }, - ), - ).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'K', - namespace: 'xx', - name: 'n', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithoutNamespace, - { - kind: 'K', - name: 'n', - }, - { defaultNamespace: 'xx' }, - ), - ).toBe(false); - - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'K', - namespace: 'ns', - name: 'x', - }), - ).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'K', - namespace: 'default', - name: 'x', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithoutNamespace, - { - kind: 'K', - name: 'x', - }, - { defaultNamespace: 'default' }, - ), - ).toBe(false); - }); - }); }); diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index 66974f5a01..cfe979423f 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -15,7 +15,7 @@ */ import { DEFAULT_NAMESPACE } from './constants'; -import { EntityName } from '../types'; +import { CompoundEntityRef } from '../types'; import { Entity } from './Entity'; function parseRefString(ref: string): { @@ -23,29 +23,47 @@ function parseRefString(ref: string): { namespace?: string; name: string; } { - const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref.trim()); - if (!match) { + let colonI = ref.indexOf(':'); + const slashI = ref.indexOf('/'); + + // If the / is ahead of the :, treat the rest as the name + if (slashI !== -1 && slashI < colonI) { + colonI = -1; + } + + const kind = colonI === -1 ? undefined : ref.slice(0, colonI); + const namespace = slashI === -1 ? undefined : ref.slice(colonI + 1, slashI); + const name = ref.slice(Math.max(colonI + 1, slashI + 1)); + + if (kind === '' || namespace === '' || name === '') { throw new TypeError( `Entity reference "${ref}" was not on the form [:][/]`, ); } - return { - kind: match[1]?.slice(0, -1), - namespace: match[2]?.slice(0, -1), - name: match[3], - }; + return { kind, namespace, name }; } /** - * Extracts the kind, namespace and name that form the name triplet of the - * given entity. + * Extracts the kind, namespace and name that form the compound entity ref + * triplet of the given entity. + * + * @public + * @deprecated Use getCompoundEntityRef instead + * @param entity - An entity + * @returns The compound entity ref + */ +export const getEntityName = getCompoundEntityRef; + +/** + * Extracts the kind, namespace and name that form the compound entity ref + * triplet of the given entity. * * @public * @param entity - An entity - * @returns The complete entity name + * @returns The compound entity ref */ -export function getEntityName(entity: Entity): EntityName { +export function getCompoundEntityRef(entity: Entity): CompoundEntityRef { return { kind: entity.kind, namespace: entity.metadata.namespace || DEFAULT_NAMESPACE, @@ -53,58 +71,6 @@ export function getEntityName(entity: Entity): EntityName { }; } -/** - * The context of defaults that entity reference parsing happens within. - * - * @public - * @deprecated type inlined, will be removed in a future release. - */ -export type EntityRefContext = { - /** The default kind, if none is given in the reference */ - defaultKind?: string; - /** The default namespace, if none is given in the reference */ - defaultNamespace?: string; -}; - -/** - * Parses an entity reference, either on string or compound form, and always - * returns a complete entity name including kind, namespace and name. - * - * @remarks - * - * This function automatically assumes the default namespace "default" unless - * otherwise specified as part of the options, and will throw an error if no - * kind was specified in the input reference and no default kind was given. - * - * @deprecated Please use parseEntityRef instead - * @public - * @param ref - The reference to parse - * @param context - The context of defaults that the parsing happens within - * @returns A complete entity name - */ -export function parseEntityName( - ref: string | { kind?: string; namespace?: string; name: string }, - context: { - /** The default kind, if none is given in the reference */ - defaultKind?: string; - /** The default namespace, if none is given in the reference */ - defaultNamespace?: string; - } = {}, -): EntityName { - const { kind, namespace, name } = parseEntityRef(ref, { - defaultNamespace: DEFAULT_NAMESPACE, - ...context, - }); - - if (!kind) { - throw new Error( - `Entity reference ${namespace}/${name} did not contain a kind`, - ); - } - - return { kind, namespace, name }; -} - /** * Parses an entity reference, either on string or compound form, and returns * a structure with a name, and optional kind and namespace. @@ -127,7 +93,7 @@ export function parseEntityRef( /** The default namespace, if none is given in the reference */ defaultNamespace?: string; }, -): EntityName { +): CompoundEntityRef { if (!ref) { throw new Error(`Entity reference must not be empty`); } @@ -203,67 +169,3 @@ export function stringifyEntityRef( 'en-US', )}/${name.toLocaleLowerCase('en-US')}`; } - -/** - * Compares an entity to either a string reference or a compound reference. - * - * @remarks - * - * The comparison is case insensitive, and all of kind, namespace, and name - * must match (after applying the optional context to the ref). - * - * @public - * @param entity - The entity to match - * @param ref - A string or compound entity ref - * @param context - An optional context of default kind and namespace, that apply - * to the ref if given - * @returns True if matching, false otherwise - * @deprecated compare using stringifyEntityRef instead. - */ -export function compareEntityToRef( - entity: Entity, - ref: - | string - | { kind?: string; namespace?: string; name: string } - | EntityName, - context?: { - /** The default kind, if none is given in the reference */ - defaultKind?: string; - /** The default namespace, if none is given in the reference */ - defaultNamespace?: string; - }, -): boolean { - const entityKind = entity.kind; - const entityNamespace = entity.metadata.namespace || DEFAULT_NAMESPACE; - const entityName = entity.metadata.name; - - let refKind: string | undefined; - let refNamespace: string | undefined; - let refName: string; - if (typeof ref === 'string') { - const parsed = parseRefString(ref); - refKind = parsed.kind || context?.defaultKind; - refNamespace = - parsed.namespace || context?.defaultNamespace || DEFAULT_NAMESPACE; - refName = parsed.name; - } else { - refKind = ref.kind || context?.defaultKind; - refNamespace = - ref.namespace || context?.defaultNamespace || DEFAULT_NAMESPACE; - refName = ref.name; - } - - if (!refKind || !refNamespace) { - throw new Error( - `Entity reference or context did not contain kind and namespace`, - ); - } - - return ( - entityKind.toLocaleLowerCase('en-US') === - refKind.toLocaleLowerCase('en-US') && - entityNamespace.toLocaleLowerCase('en-US') === - refNamespace.toLocaleLowerCase('en-US') && - entityName.toLocaleLowerCase('en-US') === refName.toLocaleLowerCase('en-US') - ); -} diff --git a/packages/catalog-model/src/entity/util.test.ts b/packages/catalog-model/src/entity/util.test.ts deleted file mode 100644 index 1c39961c0f..0000000000 --- a/packages/catalog-model/src/entity/util.test.ts +++ /dev/null @@ -1,198 +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 lodash from 'lodash'; -import { - generateEntityEtag, - generateEntityUid, - entityHasChanges, - generateUpdatedEntity, -} from './util'; -import { Entity } from './Entity'; - -describe('util', () => { - describe('generateEntityUid', () => { - it('generates randomness', () => { - expect(generateEntityUid()).not.toEqual(''); - expect(generateEntityUid()).not.toEqual(generateEntityUid()); - }); - }); - - describe('generateEntityEtag', () => { - it('generates randomness', () => { - expect(generateEntityEtag()).not.toEqual(''); - expect(generateEntityEtag()).not.toEqual(generateEntityEtag()); - }); - }); - - describe('entityHasChanges', () => { - let a: Entity; - beforeEach(() => { - a = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'name', - custom: 'custom', - labels: { - labelKey: 'labelValue', - }, - annotations: { - annotationKey: 'annotationValue', - }, - }, - spec: { - a: 'a', - }, - }; - }); - - it('happy path: clone has no changes', () => { - const b = lodash.cloneDeep(a); - expect(entityHasChanges(a, b)).toBe(false); - }); - - it('detects root field changes', () => { - let b: any = lodash.cloneDeep(a); - b.apiVersion += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.apiVersion; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.kind += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.kind; - expect(entityHasChanges(a, b)).toBe(true); - }); - - it('detects metadata changes', () => { - let b: any = lodash.cloneDeep(a); - b.metadata.name += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.metadata.custom; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.metadata.custom; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.metadata.labels.n = 'n'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.metadata.labels.labelKey += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.metadata.annotations.annotationKey += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.metadata.annotations.annotationKey; - expect(entityHasChanges(a, b)).toBe(true); - }); - - it('detects spec changes', () => { - let b: any = lodash.cloneDeep(a); - b.spec.a += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.spec.a; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.spec.n = 'n'; - expect(entityHasChanges(a, b)).toBe(true); - }); - }); - - describe('generateUpdatedEntity', () => { - let a: Entity; - let b: any; - beforeEach(() => { - a = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - uid: 'da921f56-f655-4e6e-9b8b-bb19a57818d8', - etag: 'NzY5NDA5NzQtYmEwNC00MDY0LWFiYmItNTYxYzQxM2JhZDcx', - generation: 2, - name: 'name', - custom: 'custom', - labels: { - labelKey: 'labelValue', - }, - annotations: { - annotationKey: 'annotationValue', - }, - }, - spec: { - a: 'a', - }, - }; - b = lodash.cloneDeep(a); - delete b.metadata.uid; - delete b.metadata.etag; - delete b.metadata.generation; - }); - - it('happy path: running on itself leaves it unchanged', () => { - const result = generateUpdatedEntity(a, b); - expect(result).toEqual(a); - }); - - it('bumps etag and generation when spec is changed', () => { - b.spec.a += 'a'; - const result = generateUpdatedEntity(a, b); - expect(result.metadata.uid).toEqual(a.metadata.uid); - expect(result.metadata.etag).not.toEqual(a.metadata.etag); - expect(result.metadata.generation).toEqual(a.metadata.generation! + 1); - expect(result.spec).toEqual({ a: 'aa' }); - }); - - it('bumps only etag when other things than spec are changed', () => { - b.metadata.n = 'n'; - const result = generateUpdatedEntity(a, b); - expect(result.metadata.uid).toEqual(a.metadata.uid); - expect(result.metadata.etag).not.toEqual(a.metadata.etag); - expect(result.metadata.generation).toEqual(a.metadata.generation); - expect(result.metadata.n).toEqual('n'); - }); - - it('retains new annotations', () => { - b.metadata.annotations.annotationKey = 'changedValue'; - b.metadata.annotations.newKey = 'newValue'; - const result = generateUpdatedEntity(a, b); - expect(result.metadata.uid).toEqual(a.metadata.uid); - expect(result.metadata.etag).not.toEqual(a.metadata.etag); - expect(result.metadata.generation).toEqual(a.metadata.generation); - expect(result.metadata.annotations).toEqual({ - annotationKey: 'changedValue', - newKey: 'newValue', - }); - }); - - it('retains old annotations', () => { - b.metadata.annotations.newKey = 'newValue'; - const result = generateUpdatedEntity(a, b); - expect(result.metadata.uid).toEqual(a.metadata.uid); - expect(result.metadata.etag).not.toEqual(a.metadata.etag); - expect(result.metadata.generation).toEqual(a.metadata.generation); - expect(result.metadata.annotations).toEqual({ - annotationKey: 'annotationValue', - newKey: 'newValue', - }); - }); - }); -}); diff --git a/packages/catalog-model/src/entity/util.ts b/packages/catalog-model/src/entity/util.ts deleted file mode 100644 index 78359b45f4..0000000000 --- a/packages/catalog-model/src/entity/util.ts +++ /dev/null @@ -1,133 +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 lodash from 'lodash'; -import { v4 as uuidv4 } from 'uuid'; -import { Entity, AlphaEntity } from './Entity'; - -/** - * Generates a new random UID for an entity. - * - * @public - * @returns A string with enough randomness to uniquely identify an entity - * @deprecated use `uuidv4()` instead. - */ -export function generateEntityUid(): string { - return uuidv4(); -} - -/** - * Generates a new random Etag for an entity. - * - * @public - * @returns A string with enough randomness to uniquely identify an entity - * revision - * @deprecated will be removed in a future release. - */ -export function generateEntityEtag(): string { - return Buffer.from(uuidv4(), 'utf8').toString('base64').replace(/[^\w]/g, ''); -} - -/** - * Checks whether there are any significant changes going from the previous to - * the next version of this entity. - * - * @remarks - * - * Significance, in this case, means that we do not compare generated fields - * such as uid, etag and generation. - * - * Note that this comparison does NOT take status, relations or similar into - * account. It only compares the actual input entity data, i.e. metadata and - * spec. - * - * @public - * @param previous - The old state of the entity - * @param next - The new state of the entity - * @deprecated will be removed in a future release. - */ -export function entityHasChanges(previous: Entity, next: Entity): boolean { - const e1 = lodash.cloneDeep(previous); - const e2 = lodash.cloneDeep(next); - - if (!e1.metadata.labels) { - e1.metadata.labels = {}; - } - if (!e2.metadata.labels) { - e2.metadata.labels = {}; - } - if (!e1.metadata.annotations) { - e1.metadata.annotations = {}; - } - if (!e2.metadata.annotations) { - e2.metadata.annotations = {}; - } - if (!e1.metadata.tags) { - e1.metadata.tags = []; - } - if (!e2.metadata.tags) { - e2.metadata.tags = []; - } - - // Remove generated fields - delete e1.metadata.uid; - delete e1.metadata.etag; - delete e1.metadata.generation; - delete e2.metadata.uid; - delete e2.metadata.etag; - delete e2.metadata.generation; - - // Remove things that we explicitly do not compare - delete e1.relations; - delete (e1 as AlphaEntity).status; - delete e2.relations; - delete (e2 as AlphaEntity).status; - - return !lodash.isEqual(e1, e2); -} - -/** - * Takes an old revision of an entity and a new desired state, and merges - * them into a complete new state. - * - * @remarks - * - * The previous revision is expected to be a complete model loaded from the - * catalog, including the uid, etag and generation fields. - * - * @public - * @param previous - The old state of the entity - * @param next - The new state of the entity - * @returns An entity with the merged state of both - * @deprecated will be removed in a future release. - */ -export function generateUpdatedEntity(previous: Entity, next: Entity): Entity { - const { uid, etag, generation } = previous.metadata; - if (!uid || !etag || !generation) { - throw new Error('Previous entity must have uid, etag and generation'); - } - - const result = lodash.cloneDeep(next); - - // Generated fields are copied and updated - const bumpEtag = entityHasChanges(previous, result); - const bumpGeneration = !lodash.isEqual(previous.spec, result.spec); - result.metadata.uid = uid; - result.metadata.etag = bumpEtag ? generateEntityEtag() : etag; - result.metadata.generation = bumpGeneration ? generation + 1 : generation; - - return result; -} diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index ef55337ae2..b5ccfed149 100644 --- a/packages/catalog-model/src/index.ts +++ b/packages/catalog-model/src/index.ts @@ -24,5 +24,5 @@ export * from './entity'; export { EntityPolicies } from './EntityPolicies'; export * from './kinds'; export * from './location'; -export type { EntityName, EntityRef, JSONSchema } from './types'; +export type { EntityName, CompoundEntityRef } from './types'; export * from './validation'; diff --git a/packages/catalog-model/src/location/annotation.ts b/packages/catalog-model/src/location/annotation.ts index 3ff624360e..bd282e2871 100644 --- a/packages/catalog-model/src/location/annotation.ts +++ b/packages/catalog-model/src/location/annotation.ts @@ -15,43 +15,25 @@ */ /** - * Constant storing location annotation. + * Entity annotation containing the location from which the entity is sourced. * * @public - * @deprecated use {@link ANNOTATION_LOCATION} instead. - * */ -export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; -/** - * Constant storing origin location annotation - * - * @public - * @deprecated use {@link ANNOTATION_ORIGIN_LOCATION} instead. */ -export const ORIGIN_LOCATION_ANNOTATION = - 'backstage.io/managed-by-origin-location'; +export const ANNOTATION_LOCATION = 'backstage.io/managed-by-location'; /** - * Contant storing source location annotation + * Entity annotation containing the originally sourced location which ultimately + * led to this entity being ingested. * * @public - * @deprecated use {@link ANNOTATION_SOURCE_LOCATION} instead. - * */ -export const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location'; - -/** - * Constant storing location annotation. - * - * @public */ -export const ANNOTATION_LOCATION = 'backstage.io/managed-by-location'; -/** - * Constant storing origin location annotation - * - * @public */ + */ export const ANNOTATION_ORIGIN_LOCATION = 'backstage.io/managed-by-origin-location'; /** - * Contant storing source location annotation + * Entity annotation pointing to the source (e.g. source code repository root or + * similar) for this entity. * - * @public */ + * @public + */ export const ANNOTATION_SOURCE_LOCATION = 'backstage.io/source-location'; diff --git a/packages/catalog-model/src/location/helpers.ts b/packages/catalog-model/src/location/helpers.ts index 7c73908157..8f8acc3b1d 100644 --- a/packages/catalog-model/src/location/helpers.ts +++ b/packages/catalog-model/src/location/helpers.ts @@ -18,18 +18,6 @@ import { ANNOTATION_SOURCE_LOCATION } from '.'; import { Entity, stringifyEntityRef } from '../entity'; import { ANNOTATION_LOCATION } from './annotation'; -/** - * Parses a string form location reference. - * - * @public - * @param ref - A string-form location reference, e.g. `'url:https://host'` - * @returns A location reference, e.g. `{ type: 'url', target: 'https://host' }` - * @deprecated use {@link parseLocationRef} instead - */ -export function parseLocationReference(ref: string) { - return parseLocationRef(ref); -} - /** * Parses a string form location reference. * @@ -72,21 +60,6 @@ export function parseLocationRef(ref: string): { return { type, target }; } -/** - * Turns a location reference into its string form. - * - * @public - * @param ref - A location reference, e.g. `{ type: 'url', target: 'https://host' }` - * @returns A string-form location reference, e.g. `'url:https://host'` - * @deprecated use {@link stringifyLocationRef} instead - */ -export function stringifyLocationReference(ref: { - type: string; - target: string; -}): string { - return stringifyLocationRef(ref); -} - /** * Turns a location ref into its string form. * diff --git a/packages/catalog-model/src/location/index.ts b/packages/catalog-model/src/location/index.ts index 8302ec202f..c53a1d2c83 100644 --- a/packages/catalog-model/src/location/index.ts +++ b/packages/catalog-model/src/location/index.ts @@ -18,15 +18,9 @@ export { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, ANNOTATION_SOURCE_LOCATION, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, - SOURCE_LOCATION_ANNOTATION, } from './annotation'; export { getEntitySourceLocation, parseLocationRef, - parseLocationReference, stringifyLocationRef, - stringifyLocationReference, } from './helpers'; -export type { Location, LocationSpec } from './types'; diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index 13550968be..7b3f23c64f 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -14,46 +14,22 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/types'; -import { JSONSchema7 } from 'json-schema'; - /** - * JSONSchema extendable by arbitrary JSON attributes - * - * @public - * @deprecated use JSONSchema7 from the json-schema package instead. - */ -export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue }; - -/** - * A complete entity name, with the full kind-namespace-name triplet. + * All parts of a complete entity ref, forming a full kind-namespace-name + * triplet. * * @public */ -export type EntityName = { +export type CompoundEntityRef = { kind: string; namespace: string; name: string; }; /** - * A reference by name to an entity, either as a compact string representation, - * or as a compound reference structure. - * - * @deprecated Please use string directly, or EntityName (depending on what you actually need) - * @remarks - * - * The string representation is on the form `[:][/]`. - * - * Left-out parts of the reference need to be handled by the application, - * either by rejecting the reference or by falling back to default values. + * A complete entity name, with the full kind-namespace-name triplet. * + * @deprecated Use CompoundEntityRef instead * @public */ -export type EntityRef = - | string - | { - kind?: string; - namespace?: string; - name: string; - }; +export type EntityName = CompoundEntityRef; diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts index 88e3f9b002..9a9ec8bcda 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -226,4 +226,20 @@ describe('CommonValidatorFunctions', () => { ])(`isValidString %p ? %p`, (value, result) => { expect(CommonValidatorFunctions.isValidString(value)).toBe(result); }); + + it.each([ + [null, false], + [true, false], + [7, false], + [{}, false], + ['', false], + [' ', false], + [' ', false], + ['abc', true], + [' abc ', true], + ['abc xyz', true], + ['abc xyz abc.', true], + ])(`isNonEmptyString %p ? %p`, (value, result) => { + expect(CommonValidatorFunctions.isNonEmptyString(value)).toBe(result); + }); }); diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index d0357660fc..8c814412c1 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -98,6 +98,7 @@ export class CommonValidatorFunctions { /** * Checks that the value is a valid tag. * + * @deprecated This will be removed in a future release * @param value - The value to check */ static isValidTag(value: unknown): boolean { @@ -110,7 +111,7 @@ export class CommonValidatorFunctions { } /** - * Checks that the value is a valid URL. + * Checks that the value is a valid string URL. * * @param value - The value to check */ @@ -131,9 +132,19 @@ export class CommonValidatorFunctions { /** * Checks that the value is a non empty string value. * + * @deprecated use isNonEmptyString instead * @param value - The value to check */ static isValidString(value: unknown): boolean { return typeof value === 'string' && value?.trim()?.length >= 1; } + + /** + * Checks that the value is a string value that's not empty. + * + * @param value - The value to check + */ + static isNonEmptyString(value: unknown): value is string { + return typeof value === 'string' && value?.trim()?.length >= 1; + } } diff --git a/packages/catalog-model/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts index ad3f52e563..c3c7182680 100644 --- a/packages/catalog-model/src/validation/makeValidator.ts +++ b/packages/catalog-model/src/validation/makeValidator.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { CommonValidatorFunctions } from './CommonValidatorFunctions'; import { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions'; import { Validators } from './types'; @@ -27,7 +26,15 @@ const defaultValidators: Validators = { isValidLabelValue: KubernetesValidatorFunctions.isValidLabelValue, isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey, isValidAnnotationValue: KubernetesValidatorFunctions.isValidAnnotationValue, - isValidTag: CommonValidatorFunctions.isValidTag, + isValidTag: (value: unknown): boolean => { + // NOTE(freben): This one is a bit of an oddball and doesn't fit well anywhere to delegate to, so it's just inlined for now. + return ( + typeof value === 'string' && + value.length >= 1 && + value.length <= 63 && + /^[a-z0-9:+#]+(\-[a-z0-9:+#]+)*$/.test(value) + ); + }, }; /** diff --git a/packages/cli-common/.eslintrc.js b/packages/cli-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/cli-common/.eslintrc.js +++ b/packages/cli-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli/.eslintrc.js b/packages/cli/.eslintrc.js index 69bec6cd2a..e9ea2f5bfb 100644 --- a/packages/cli/.eslintrc.js +++ b/packages/cli/.eslintrc.js @@ -1,7 +1,6 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { ignorePatterns: ['templates/**'], rules: { 'no-console': 0, }, -}; +}); diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 46b67615c4..b3ab3fc0e7 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/cli +## 0.15.2-next.0 + +### Patch Changes + +- 2c528506aa: Added `--since ` flag for `repo build` command.` +- d3d1b82198: chore(deps): bump `minimatch` from 5.0.0 to 5.0.1 +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- 44cc7c3b95: Added a new ESLint configuration setup for packages, which utilizes package roles to generate the correct configuration. The new configuration is available at `@backstage/cli/config/eslint-factory`. + + Introduced a new `backstage-cli migrate package-lint-configs` command, which migrates old lint configurations to use `@backstage/cli/config/eslint-factory`. + +- b1aacbf96a: Applied the fix from version `0.15.1` of this package, which was part of the `v0.70.1` release of Backstage. +- d2ecde959b: Package roles are now marked as stable and migration is encouraged. Please check out the [migration guide](https://backstage.io/docs/tutorials/package-role-migration). + + The new `package`, `repo`, and `migrate` command categories are now marked as stable. + + Marked all commands that are being replaced by the new `package` and `repo` commands as deprecated. + + The package templates used by the `create` command have all been updated to use package roles. + +- f06da37290: The backend development setup now ignores the `"browser"` and `"module"` entry points in `package.json`, and instead always uses `"main"`. +- 6a1fe077ad: Changed the logic for how modules are marked as external in the Rollup build of packages. Rather than only marking dependencies and build-in Node.js modules as external, all non-relative imports are now considered external. +- dc6002a7b9: The `--since` flag of repo commands now silently falls back to using the provided `ref` directly if no merge base is available. +- Updated dependencies + - @backstage/config-loader@0.9.7-next.0 + +## 0.15.1 + +### Patch Changes + +- Fixed an issue where the release stage entry point of packages were not resolved correctly. + +## 0.15.0 + +### Minor Changes + +- 8c3f30cb28: **BREAKING**: Removed the deprecated `app.` template variables from the `index.html` templating. These should be replaced by using `config.getString("app.")` instead. + +### Patch Changes + +- 46a19c599f: The CLI now bundles both version 16 and 17 of the patched `@hot-loader/react-dom` dependency, and selects the appropriate one based on what version of `react-dom` is installed within the app. + ## 0.14.1 ### Patch Changes diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js new file mode 100644 index 0000000000..67f30ac359 --- /dev/null +++ b/packages/cli/config/eslint-factory.js @@ -0,0 +1,286 @@ +/* + * 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. + */ + +const { join: joinPath } = require('path'); + +/** + * Creates a ESLint configuration that extends the base Backstage configuration. + * In addition to the standard ESLint configuration options, the `extraConfig` + * parameter also accepts the following keys: + * + * - `tsRules`: Additional ESLint rules to apply to TypeScript + * - `testRules`: Additional ESLint rules to apply to tests + * - `restrictedImports`: Additional paths to add to no-restricted-imports + * - `restrictedSrcImports`: Additional paths to add to no-restricted-imports in src files + * - `restrictedTestImports`: Additional paths to add to no-restricted-imports in test files + * - `restrictedSyntax`: Additional patterns to add to no-restricted-syntax + * - `restrictedSrcSyntax`: Additional patterns to add to no-restricted-syntax in src files + * - `restrictedTestSyntax`: Additional patterns to add to no-restricted-syntax in test files + */ +function createConfig(dir, extraConfig = {}) { + const { + extends: extraExtends, + plugins, + env, + parserOptions, + ignorePatterns, + + rules, + tsRules, + testRules, + + restrictedImports, + restrictedSrcImports, + restrictedTestImports, + restrictedSyntax, + restrictedSrcSyntax, + restrictedTestSyntax, + + ...otherConfig + } = extraConfig; + + return { + ...otherConfig, + extends: [ + '@spotify/eslint-config-base', + '@spotify/eslint-config-typescript', + 'prettier', + 'plugin:jest/recommended', + 'plugin:monorepo/recommended', + ...(extraExtends ?? []), + ], + parser: '@typescript-eslint/parser', + plugins: ['import', ...(plugins ?? [])], + env: { + jest: true, + ...env, + }, + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + lib: require('./tsconfig.json').compilerOptions.lib, + ...parserOptions, + }, + ignorePatterns: [ + '.eslintrc.js', + '**/dist/**', + '**/dist-types/**', + ...(ignorePatterns ?? []), + ], + rules: { + 'no-shadow': 'off', + 'no-redeclare': 'off', + '@typescript-eslint/no-shadow': 'error', + '@typescript-eslint/no-redeclare': 'error', + 'no-undef': 'off', + 'import/newline-after-import': 'error', + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: dir + ? [ + `!${joinPath(dir, 'src/**')}`, + joinPath(dir, 'src/**/*.test.*'), + joinPath(dir, 'src/**/*.stories.*'), + joinPath(dir, 'src/setupTests.*'), + ] + : [ + // Legacy config for packages that don't provide a dir + '**/*.test.*', + '**/*.stories.*', + '**/src/setupTests.*', + '**/dev/**', + ], + optionalDependencies: true, + peerDependencies: true, + bundledDependencies: true, + }, + ], + 'no-unused-expressions': 'off', + '@typescript-eslint/no-unused-expressions': 'error', + '@typescript-eslint/consistent-type-assertions': 'error', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + vars: 'all', + args: 'after-used', + ignoreRestSiblings: true, + argsIgnorePattern: '^_', + }, + ], + + 'no-restricted-imports': [ + 2, + { + paths: [ + ...(restrictedImports ?? []), + ...(restrictedSrcImports ?? []), + ], + // Avoid cross-package imports + patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], + }, + ], + + 'no-restricted-syntax': [ + 'error', + ...(restrictedSyntax ?? []), + ...(restrictedSrcSyntax ?? []), + ], + + ...rules, + }, + overrides: [ + { + files: ['**/*.ts?(x)'], + rules: { + '@typescript-eslint/no-unused-vars': 'off', + 'no-undef': 'off', + ...tsRules, + }, + }, + { + files: ['**/*.test.*', '**/*.stories.*', 'src/setupTests.*', '!src/**'], + rules: { + ...testRules, + 'no-restricted-syntax': [ + 'error', + ...(restrictedSyntax ?? []), + ...(restrictedTestSyntax ?? []), + ], + 'no-restricted-imports': [ + 2, + { + paths: [ + ...(restrictedImports ?? []), + ...(restrictedTestImports ?? []), + ], + // Avoid cross-package imports + patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], + }, + ], + }, + }, + ], + }; +} + +/** + * Creates a ESLint configuration for the given package role. + * The `extraConfig` parameter accepts the same keys as for the `createConfig` function. + */ +function createConfigForRole(dir, role, extraConfig = {}) { + switch (role) { + case 'common-library': + return createConfig(dir, extraConfig); + + case 'web-library': + case 'frontend': + case 'frontend-plugin': + case 'frontend-plugin-module': + return createConfig(dir, { + ...extraConfig, + extends: [ + '@spotify/eslint-config-react', + ...(extraConfig.extends ?? []), + ], + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ...extraConfig.parserOptions, + }, + settings: { + react: { + version: 'detect', + }, + ...extraConfig.settings, + }, + restrictedImports: [ + { + // Importing the entire MUI icons packages kills build performance as the list of icons is huge. + name: '@material-ui/icons', + message: "Please import '@material-ui/icons/' instead.", + }, + { + name: '@material-ui/icons/', // because this is possible too ._. + message: "Please import '@material-ui/icons/' instead.", + }, + ...require('module').builtinModules, + ...(extraConfig.restrictedImports ?? []), + ], + tsRules: { + 'react/prop-types': 0, + ...extraConfig.tsRules, + }, + }); + + case 'cli': + case 'node-library': + case 'backend': + case 'backend-plugin': + case 'backend-plugin-module': + return createConfig(dir, { + ...extraConfig, + globals: { + __non_webpack_require__: 'readonly', + ...extraConfig.globals, + }, + rules: { + 'no-console': 0, // Permitted in console programs + 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' + ...extraConfig.rules, + }, + restrictedSyntax: [ + { + message: + 'Default import from winston is not allowed, import `* as winston` instead.', + selector: + 'ImportDeclaration[source.value="winston"] ImportDefaultSpecifier', + }, + ...(extraConfig.restrictedSyntax ?? []), + ], + restrictedSrcSyntax: [ + { + message: + "`__dirname` doesn't refer to the same dir in production builds, try `resolvePackagePath()` from `@backstage/backend-common` instead.", + selector: 'Identifier[name="__dirname"]', + }, + ...(extraConfig.restrictedSrcSyntax ?? []), + ], + }); + default: + throw new Error(`Unknown package role ${role}`); + } +} + +/** + * Creates a ESLint configuration for the package in the given directory. + * The `extraConfig` parameter accepts the same keys as for the `createConfig` function. + */ +function createPackageConfig(dir, extraConfig) { + const pkg = require(joinPath(dir, 'package.json')); + const role = pkg.backstage?.role; + if (!role) { + throw new Error(`Package ${pkg.name} does not have a backstage.role`); + } + + return createConfigForRole(dir, role, extraConfig); +} + +module.exports = createPackageConfig; // Alias +module.exports.createConfig = createConfig; +module.exports.createConfigForRole = createConfigForRole; +module.exports.createPackageConfig = createPackageConfig; diff --git a/packages/cli/config/eslint.backend.js b/packages/cli/config/eslint.backend.js index 519b388b96..1851312be5 100644 --- a/packages/cli/config/eslint.backend.js +++ b/packages/cli/config/eslint.backend.js @@ -14,90 +14,7 @@ * limitations under the License. */ -const globalRestrictedSyntax = [ - { - message: - 'Default import from winston is not allowed, import `* as winston` instead.', - selector: - 'ImportDeclaration[source.value="winston"] ImportDefaultSpecifier', - }, -]; - -module.exports = { - extends: [ - '@spotify/eslint-config-base', - '@spotify/eslint-config-typescript', - 'prettier', - 'plugin:jest/recommended', - 'plugin:monorepo/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['import'], - env: { - jest: true, - }, - globals: { - __non_webpack_require__: 'readonly', - }, - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - lib: require('./tsconfig.json').compilerOptions.lib, - }, - ignorePatterns: ['.eslintrc.js', '**/dist/**', '**/dist-types/**'], - rules: { - 'no-shadow': 'off', - 'no-redeclare': 'off', - '@typescript-eslint/no-shadow': 'error', - '@typescript-eslint/no-redeclare': 'error', - - 'no-console': 0, // Permitted in console programs - 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' - 'import/newline-after-import': 'error', - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: ['**/*.test.*', 'src/setupTests.*', 'dev/**'], - optionalDependencies: true, - peerDependencies: true, - bundledDependencies: true, - }, - ], - 'no-unused-expressions': 'off', - '@typescript-eslint/no-unused-expressions': 'error', - '@typescript-eslint/no-unused-vars': [ - 'warn', - { vars: 'all', args: 'after-used', ignoreRestSiblings: true }, - ], - // Avoid cross-package imports - 'no-restricted-imports': [ - 2, - { patterns: ['**/../../**/*/src/**', '**/../../**/*/src'] }, - ], - // Avoid default import from winston as it breaks at runtime - 'no-restricted-syntax': [ - 'error', - { - message: - "`__dirname` doesn't refer to the same dir in production builds, try `resolvePackagePath()` from `@backstage/backend-common` instead.", - selector: 'Identifier[name="__dirname"]', - }, - ...globalRestrictedSyntax, - ], - }, - overrides: [ - { - files: ['**/*.ts?(x)'], - rules: { - '@typescript-eslint/no-unused-vars': 'off', - 'no-undef': 'off', - }, - }, - { - files: ['*.test.*', 'src/setupTests.*', 'dev/**'], - rules: { - 'no-restricted-syntax': ['error', ...globalRestrictedSyntax], - }, - }, - ], -}; +module.exports = require('./eslint-factory').createConfigForRole( + undefined, + 'node-library', +); diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index 5555dad451..9258454837 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -14,96 +14,7 @@ * limitations under the License. */ -module.exports = { - extends: [ - '@spotify/eslint-config-base', - '@spotify/eslint-config-react', - '@spotify/eslint-config-typescript', - 'prettier', - 'plugin:jest/recommended', - 'plugin:monorepo/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['import'], - env: { - jest: true, - }, - parserOptions: { - ecmaVersion: 2018, - ecmaFeatures: { - jsx: true, - }, - sourceType: 'module', - lib: require('./tsconfig.json').compilerOptions.lib, - }, - settings: { - react: { - version: 'detect', - }, - }, - ignorePatterns: ['.eslintrc.js', '**/dist/**', '**/dist-types/**'], - rules: { - 'no-shadow': 'off', - 'no-redeclare': 'off', - '@typescript-eslint/no-shadow': 'error', - '@typescript-eslint/no-redeclare': 'error', - 'no-undef': 'off', - 'import/newline-after-import': 'error', - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: [ - '**/*.test.*', - '**/*.stories.*', - '**/src/setupTests.*', - '**/dev/**', - ], - optionalDependencies: true, - peerDependencies: true, - bundledDependencies: true, - }, - ], - 'no-unused-expressions': 'off', - '@typescript-eslint/no-unused-expressions': 'error', - '@typescript-eslint/consistent-type-assertions': 'error', - '@typescript-eslint/no-unused-vars': [ - 'warn', - { - vars: 'all', - args: 'after-used', - ignoreRestSiblings: true, - argsIgnorePattern: '^_', - }, - ], - 'no-restricted-imports': [ - 2, - { - paths: [ - { - // Importing the entire MUI icons packages kills build performance as the list of icons is huge. - name: '@material-ui/icons', - message: "Please import '@material-ui/icons/' instead.", - }, - { - name: '@material-ui/icons/', // because this is possible too ._. - message: "Please import '@material-ui/icons/' instead.", - }, - ...require('module').builtinModules, - ], - // Avoid cross-package imports - patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], - }, - ], - }, - overrides: [ - { - files: ['**/*.ts?(x)'], - rules: { - // Default to not enforcing prop-types in typescript - 'react/prop-types': 0, - '@typescript-eslint/no-unused-vars': 'off', - 'no-undef': 'off', - }, - }, - ], -}; +module.exports = require('./eslint-factory').createConfigForRole( + undefined, + 'web-library', +); diff --git a/packages/cli/package.json b/packages/cli/package.json index b17d517ba1..46ce22f98c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.14.1", + "version": "0.15.2-next.0", "private": false, "publishConfig": { "access": "public" @@ -33,11 +33,12 @@ "dependencies": { "@backstage/cli-common": "^0.1.8", "@backstage/config": "^0.1.15", - "@backstage/config-loader": "^0.9.6", + "@backstage/config-loader": "^0.9.7-next.0", "@backstage/errors": "^0.2.2", "@backstage/release-manifests": "^0.0.2", "@backstage/types": "^0.1.3", - "@hot-loader/react-dom": "^17.0.2", + "@hot-loader/react-dom-v16": "npm:@hot-loader/react-dom@^16.0.2", + "@hot-loader/react-dom-v17": "npm:@hot-loader/react-dom@^17.0.2", "@manypkg/get-packages": "^1.1.3", "@octokit/request": "^5.4.12", "@rollup/plugin-commonjs": "^21.0.1", @@ -78,7 +79,7 @@ "eslint-plugin-react-hooks": "^4.3.0", "express": "^4.17.1", "fork-ts-checker-webpack-plugin": "^7.0.0-alpha.8", - "fs-extra": "9.1.0", + "fs-extra": "10.0.1", "glob": "^7.1.7", "handlebars": "^4.7.3", "html-webpack-plugin": "^5.3.1", @@ -88,7 +89,7 @@ "json-schema": "^0.4.0", "jest-transform-yaml": "^1.0.0", "lodash": "^4.17.21", - "minimatch": "5.0.0", + "minimatch": "5.0.1", "mini-css-extract-plugin": "^2.4.2", "npm-packlist": "^3.0.0", "node-libs-browser": "^2.2.1", @@ -102,7 +103,6 @@ "rollup": "^2.60.2", "rollup-plugin-dts": "^4.0.1", "rollup-plugin-esbuild": "^4.7.2", - "rollup-plugin-peer-deps-external": "^2.2.2", "rollup-plugin-postcss": "^4.0.0", "rollup-pluginutils": "^2.8.2", "run-script-webpack-plugin": "^0.0.11", @@ -121,13 +121,13 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@backstage/theme": "^0.2.15", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index d3baa37b3f..bc427bb19b 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -27,10 +27,8 @@ const configOption = [ export function registerRepoCommand(program: CommanderStatic) { const command = program - .command('repo [command]', { hidden: true }) - .description( - 'Command that run across an entire Backstage project [EXPERIMENTAL]', - ); + .command('repo [command]') + .description('Command that run across an entire Backstage project'); command .command('build') @@ -41,6 +39,10 @@ export function registerRepoCommand(program: CommanderStatic) { '--all', 'Build all packages, including bundled app and backend packages.', ) + .option( + '--since ', + 'Only build packages and their dev dependents that changed since the specified ref', + ) .action(lazy(() => import('./repo/build').then(m => m.command))); command @@ -61,17 +63,14 @@ export function registerRepoCommand(program: CommanderStatic) { export function registerScriptCommand(program: CommanderStatic) { const command = program - .command('package [command]', { hidden: true }) - .description('Lifecycle scripts for individual packages [EXPERIMENTAL]'); + .command('package [command]') + .description('Lifecycle scripts for individual packages'); command .command('start') .description('Start a package for local development') .option(...configOption) - .option( - '--role ', - 'Run the command with an explicit package role [EXPERIMENTAL]', - ) + .option('--role ', 'Run the command with an explicit package role') .option('--check', 'Enable type checking and linting if available') .option('--inspect', 'Enable debugger in Node.js environments') .option( @@ -83,10 +82,7 @@ export function registerScriptCommand(program: CommanderStatic) { command .command('build') .description('Build a package for production deployment or publishing') - .option( - '--role ', - 'Run the command with an explicit package role [EXPERIMENTAL]', - ) + .option('--role ', 'Run the command with an explicit package role') .option( '--minify', 'Minify the generated code. Does not apply to app or backend packages.', @@ -147,8 +143,8 @@ export function registerScriptCommand(program: CommanderStatic) { export function registerMigrateCommand(program: CommanderStatic) { const command = program - .command('migrate [command]', { hidden: true }) - .description('Migration utilities [EXPERIMENTAL]'); + .command('migrate [command]') + .description('Migration utilities'); command .command('package-roles') @@ -161,13 +157,22 @@ export function registerMigrateCommand(program: CommanderStatic) { .action( lazy(() => import('./migrate/packageScripts').then(m => m.command)), ); + + command + .command('package-lint-configs') + .description( + 'Migrates all packages to use @backstage/cli/config/eslint-factory', + ) + .action( + lazy(() => import('./migrate/packageLintConfigs').then(m => m.command)), + ); } export function registerCommands(program: CommanderStatic) { // TODO(Rugvip): Deprecate in favor of package variant program .command('app:build') - .description('Build an app for a production release') + .description('Build an app for a production release [DEPRECATED]') .option('--stats', 'Write bundle stats to output directory') .option(...configOption) .action(lazy(() => import('./app/build').then(m => m.default))); @@ -175,7 +180,7 @@ export function registerCommands(program: CommanderStatic) { // TODO(Rugvip): Deprecate in favor of package variant program .command('app:serve') - .description('Serve an app for local development') + .description('Serve an app for local development [DEPRECATED]') .option('--check', 'Enable type checking and linting') .option(...configOption) .action(lazy(() => import('./app/serve').then(m => m.default))); @@ -183,7 +188,7 @@ export function registerCommands(program: CommanderStatic) { // TODO(Rugvip): Deprecate in favor of package variant program .command('backend:build') - .description('Build a backend plugin') + .description('Build a backend plugin [DEPRECATED]') .option('--minify', 'Minify the generated code') .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./backend/build').then(m => m.default))); @@ -191,7 +196,7 @@ export function registerCommands(program: CommanderStatic) { // TODO(Rugvip): Deprecate in favor of package variant program .command('backend:bundle') - .description('Bundle the backend into a deployment archive') + .description('Bundle the backend into a deployment archive [DEPRECATED]') .option( '--build-dependencies', 'Build all local package dependencies before bundling the backend', @@ -201,7 +206,9 @@ export function registerCommands(program: CommanderStatic) { // TODO(Rugvip): Deprecate in favor of package variant program .command('backend:dev') - .description('Start local development server with HMR for the backend') + .description( + 'Start local development server with HMR for the backend [DEPRECATED]', + ) .option('--check', 'Enable type checking and linting') .option('--inspect', 'Enable debugger') .option('--inspect-brk', 'Enable debugger with await to attach debugger') @@ -250,7 +257,7 @@ export function registerCommands(program: CommanderStatic) { // TODO(Rugvip): Deprecate in favor of package variant program .command('plugin:build') - .description('Build a plugin') + .description('Build a plugin [DEPRECATED]') .option('--minify', 'Minify the generated code') .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./plugin/build').then(m => m.default))); @@ -258,7 +265,7 @@ export function registerCommands(program: CommanderStatic) { // TODO(Rugvip): Deprecate in favor of package variant program .command('plugin:serve') - .description('Serves the dev/ folder of a plugin') + .description('Serves the dev/ folder of a plugin [DEPRECATED]') .option('--check', 'Enable type checking and linting') .option(...configOption) .action(lazy(() => import('./plugin/serve').then(m => m.default))); @@ -273,7 +280,7 @@ export function registerCommands(program: CommanderStatic) { // TODO(Rugvip): Deprecate in favor of package variant program .command('build') - .description('Build a package for publishing') + .description('Build a package for publishing [DEPRECATED]') .option('--outputs ', 'List of formats to output [types,cjs,esm]') .option('--minify', 'Minify the generated code') .option('--experimental-type-build', 'Enable experimental type build') @@ -288,7 +295,7 @@ export function registerCommands(program: CommanderStatic) { 'eslint-formatter-friendly', ) .option('--fix', 'Attempt to automatically fix violations') - .description('Lint a package') + .description('Lint a package [DEPRECATED]') .action(lazy(() => import('./lint').then(m => m.default))); // TODO(Rugvip): Deprecate in favor of package variant @@ -296,7 +303,9 @@ export function registerCommands(program: CommanderStatic) { .command('test') .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args .helpOption(', --backstage-cli-help') // Let Jest handle help - .description('Run tests, forwarding args to Jest, defaulting to watch mode') + .description( + 'Run tests, forwarding args to Jest, defaulting to watch mode [DEPRECATED]', + ) .action(lazy(() => import('./testCommand').then(m => m.default))); program @@ -380,19 +389,23 @@ export function registerCommands(program: CommanderStatic) { // TODO(Rugvip): Deprecate in favor of package variant program .command('prepack') - .description('Prepares a package for packaging before publishing') + .description( + 'Prepares a package for packaging before publishing [DEPRECATED]', + ) .action(lazy(() => import('./pack').then(m => m.pre))); // TODO(Rugvip): Deprecate in favor of package variant program .command('postpack') - .description('Restores the changes made by the prepack command') + .description( + 'Restores the changes made by the prepack command [DEPRECATED]', + ) .action(lazy(() => import('./pack').then(m => m.post))); // TODO(Rugvip): Deprecate in favor of package variant program .command('clean') - .description('Delete cache directories') + .description('Delete cache directories [DEPRECATED]') .action(lazy(() => import('./clean/clean').then(m => m.default))); program diff --git a/packages/cli/src/commands/migrate/packageLintConfigs.ts b/packages/cli/src/commands/migrate/packageLintConfigs.ts new file mode 100644 index 0000000000..c0007ef59d --- /dev/null +++ b/packages/cli/src/commands/migrate/packageLintConfigs.ts @@ -0,0 +1,89 @@ +/* + * 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 { resolve as resolvePath } from 'path'; +import { PackageGraph } from '../../lib/monorepo'; +import { runPlain } from '../../lib/run'; + +const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`; + +export async function command() { + const packages = await PackageGraph.listTargetPackages(); + + const oldConfigs = [ + require.resolve('@backstage/cli/config/eslint.js'), + require.resolve('@backstage/cli/config/eslint.backend.js'), + ]; + + const configPaths = new Array(); + await Promise.all( + packages.map(async ({ dir, packageJson }) => { + const configPath = resolvePath(dir, '.eslintrc.js'); + if (!(await fs.pathExists(configPath))) { + console.log(`Skipping ${packageJson.name}, missing .eslintrc.js`); + return; + } + let existingConfig: Record; + try { + existingConfig = require(configPath); + } catch (error) { + console.log( + `Skipping ${packageJson.name}, failed to load .eslintrc.js, ${error}`, + ); + return; + } + + // Only transform configs that extend the old config files, and + // we remove that entry from the extends array. + const extendsArray = (existingConfig.extends as string[]) ?? []; + const extendIndex = extendsArray.findIndex(p => oldConfigs.includes(p)); + if (extendIndex === -1) { + console.log( + `Skipping ${packageJson.name}, .eslintrc.js does not extend the legacy config`, + ); + return; + } + extendsArray.splice(extendIndex, 1); + if (extendsArray.length === 0) { + delete existingConfig.extends; + } + + if (Object.keys(existingConfig).length > 0) { + await fs.writeFile( + configPath, + `${PREFIX}(__dirname, ${JSON.stringify(existingConfig, null, 2)});\n`, + ); + } else { + await fs.writeFile(configPath, `${PREFIX}(__dirname);\n`); + } + configPaths.push(configPath); + }), + ); + + // If prettier is present, then we run that too + let hasPrettier = false; + try { + require.resolve('prettier'); + hasPrettier = true; + } catch { + /* ignore */ + } + + if (hasPrettier) { + await runPlain('prettier', '--write', ...configPaths); + } +} diff --git a/packages/cli/src/commands/pack.ts b/packages/cli/src/commands/pack.ts index e646568980..6c88ac055f 100644 --- a/packages/cli/src/commands/pack.ts +++ b/packages/cli/src/commands/pack.ts @@ -23,6 +23,11 @@ const SKIPPED_KEYS = ['access', 'registry', 'tag', 'alphaTypes', 'betaTypes']; const PKG_PATH = 'package.json'; const PKG_BACKUP_PATH = 'package.json-prepack'; +function resolveEntrypoint(pkg: any, name: string) { + const targetEntry = pkg.publishConfig[name] || pkg[name]; + return targetEntry && joinPath('..', targetEntry); +} + // Writes e.g. alpha/package.json async function writeReleaseStageEntrypoint(pkg: any, stage: 'alpha' | 'beta') { await fs.ensureDir(paths.resolveTarget(stage)); @@ -31,9 +36,9 @@ async function writeReleaseStageEntrypoint(pkg: any, stage: 'alpha' | 'beta') { { name: pkg.name, version: pkg.version, - main: (pkg.publishConfig.main || pkg.main) && '..', - module: (pkg.publishConfig.module || pkg.module) && '..', - browser: (pkg.publishConfig.browser || pkg.browser) && '..', + main: resolveEntrypoint(pkg, 'main'), + module: resolveEntrypoint(pkg, 'module'), + browser: resolveEntrypoint(pkg, 'browser'), types: joinPath('..', pkg.publishConfig[`${stage}Types`]), }, { encoding: 'utf8', spaces: 2 }, diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts index ceb468ebfa..fe50bee84f 100644 --- a/packages/cli/src/commands/repo/build.ts +++ b/packages/cli/src/commands/repo/build.ts @@ -78,7 +78,18 @@ function createScriptOptionsParser(anyCmd: Command, commandPath: string[]) { } export async function command(cmd: Command): Promise { - const packages = await PackageGraph.listTargetPackages(); + let packages = await PackageGraph.listTargetPackages(); + + if (cmd.since) { + const graph = PackageGraph.fromPackages(packages); + const changedPackages = await graph.listChangedPackages({ ref: cmd.since }); + const withDevDependents = graph.collectPackageNames( + changedPackages.map(pkg => pkg.name), + pkg => pkg.localDevDependents.keys(), + ); + packages = Array.from(withDevDependents).map(name => graph.get(name)!); + } + const apps = new Array(); const backends = new Array(); diff --git a/packages/cli/src/lib/builder/config.test.ts b/packages/cli/src/lib/builder/config.test.ts new file mode 100644 index 0000000000..a2e99da73b --- /dev/null +++ b/packages/cli/src/lib/builder/config.test.ts @@ -0,0 +1,59 @@ +/* + * 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 { ExternalOption } from 'rollup'; +import { makeRollupConfigs } from './config'; +import { Output } from './types'; + +describe('makeRollupConfigs', () => { + it('should mark external modules correctly', async () => { + const importerPath = '/some/path.ts'; // when specified we don't care about the path + + const [config] = await makeRollupConfigs({ + outputs: new Set([Output.cjs]), + }); + const external = config.external as Exclude< + ExternalOption, + string | RegExp | (string | RegExp)[] + >; + + expect(external('foo', importerPath, false)).toBe(true); + expect(external('./foo', importerPath, false)).toBe(false); + expect(external('/foo', importerPath, false)).toBe(false); + expect(external('.\\foo', importerPath, false)).toBe(false); + expect(external('c:\\foo', importerPath, false)).toBe(false); + expect(external('@foo/bar', importerPath, false)).toBe(true); + expect(external('../foo', importerPath, false)).toBe(false); + + // Modules without an importer are entry points, i.e. not external + expect(external('foo', undefined, false)).toBe(false); + expect(external('./foo', undefined, false)).toBe(false); + expect(external('/foo', undefined, false)).toBe(false); + expect(external('.\\foo', undefined, false)).toBe(false); + expect(external('c:\\foo', undefined, false)).toBe(false); + expect(external('@foo/bar', undefined, false)).toBe(false); + expect(external('../foo', undefined, false)).toBe(false); + + // After modules have been resolved they're never marked as external + expect(external('foo', importerPath, true)).toBe(false); + expect(external('./foo', importerPath, true)).toBe(false); + expect(external('/foo', importerPath, true)).toBe(false); + expect(external('.\\foo', importerPath, true)).toBe(false); + expect(external('c:\\foo', importerPath, true)).toBe(false); + expect(external('@foo/bar', importerPath, true)).toBe(false); + expect(external('../foo', importerPath, true)).toBe(false); + }); +}); diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 0a214f1a7a..fe91cedbb0 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -17,7 +17,6 @@ import chalk from 'chalk'; import fs from 'fs-extra'; import { relative as relativePath, resolve as resolvePath } from 'path'; -import peerDepsExternal from 'rollup-plugin-peer-deps-external'; import commonjs from '@rollup/plugin-commonjs'; import resolve from '@rollup/plugin-node-resolve'; import postcss from 'rollup-plugin-postcss'; @@ -33,6 +32,19 @@ import { BuildOptions, Output } from './types'; import { paths } from '../paths'; import { svgrTemplate } from '../svgrTemplate'; +function isFileImport(source: string) { + if (source.startsWith('.')) { + return true; + } + if (source.startsWith('/')) { + return true; + } + if (source.match(/[a-z]:/i)) { + return true; + } + return false; +} + export async function makeRollupConfigs( options: BuildOptions, ): Promise { @@ -81,12 +93,10 @@ export async function makeRollupConfigs( output, onwarn, preserveEntrySignatures: 'strict', - external: require('module').builtinModules, + // All module imports are always marked as external + external: (source, importer, isResolved) => + Boolean(importer && !isResolved && !isFileImport(source)), plugins: [ - peerDepsExternal({ - packageJsonPath: resolvePath(targetDir, 'package.json'), - includeDependencies: true, - }), resolve({ mainFields }), commonjs({ include: /node_modules/, diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 3cfcafc005..575af4ea8b 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -15,7 +15,6 @@ */ import fs from 'fs-extra'; -import chalk from 'chalk'; import { resolve as resolvePath } from 'path'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; @@ -118,43 +117,11 @@ export async function createConfig( }), ); - const appParamDeprecationMsg = chalk.red( - 'DEPRECATION WARNING: using `app.` in the index.html template is deprecated, use `config.getString("app.")` instead.', - ); plugins.push( new HtmlWebpackPlugin({ template: paths.targetHtml, templateParameters: { publicPath: validBaseUrl.pathname.replace(/\/$/, ''), - app: { - get title() { - console.warn(appParamDeprecationMsg); - return frontendConfig.getString('app.title'); - }, - get baseUrl() { - console.warn(appParamDeprecationMsg); - return validBaseUrl.href; - }, - get googleAnalyticsTrackingId() { - console.warn(appParamDeprecationMsg); - return frontendConfig.getOptionalString( - 'app.googleAnalyticsTrackingId', - ); - }, - get datadogRum() { - console.warn(appParamDeprecationMsg); - return { - env: frontendConfig.getOptionalString('app.datadogRum.env'), - clientToken: frontendConfig.getOptionalString( - 'app.datadogRum.clientToken', - ), - applicationId: frontendConfig.getOptionalString( - 'app.datadogRum.applicationId', - ), - site: frontendConfig.getOptionalString('app.datadogRum.site'), - }; - }, - }, config: frontendConfig, }, }), @@ -167,13 +134,16 @@ export async function createConfig( }), ); + // Detect and use the appropriate react-dom hot-loader patch based on what + // version of React is used within the target repo. const resolveAliases: Record = {}; try { // eslint-disable-next-line import/no-extraneous-dependencies const { version: reactDomVersion } = require('react-dom/package.json'); - // Only apply the alias for hook support if we're running with React 16 if (reactDomVersion.startsWith('16.')) { - resolveAliases['react-dom'] = '@hot-loader/react-dom'; + resolveAliases['react-dom'] = '@hot-loader/react-dom-v16'; + } else { + resolveAliases['react-dom'] = '@hot-loader/react-dom-v17'; } } catch (error) { console.warn(`WARNING: Failed to read react-dom version, ${error}`); @@ -302,7 +272,7 @@ export async function createBackendConfig( ], resolve: { extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'], - mainFields: ['browser', 'module', 'main'], + mainFields: ['main'], modules: [paths.rootNodeModules, ...moduleDirs], plugins: [ new LinkedPackageResolvePlugin(paths.rootNodeModules, externalPkgs), diff --git a/packages/cli/src/lib/git.ts b/packages/cli/src/lib/git.ts index f8783804d1..074260b38f 100644 --- a/packages/cli/src/lib/git.ts +++ b/packages/cli/src/lib/git.ts @@ -50,9 +50,16 @@ export async function listChangedFiles(ref: string) { if (!ref) { throw new Error('ref is required'); } - const [base] = await runGit('merge-base', 'HEAD', ref); - const tracked = await runGit('diff', '--name-only', base); + let diffRef = ref; + try { + const [base] = await runGit('merge-base', 'HEAD', ref); + diffRef = base; + } catch { + // silently fall back to using the ref directly if merge base is not available + } + + const tracked = await runGit('diff', '--name-only', diffRef); const untracked = await runGit('ls-files', '--others', '--exclude-standard'); return Array.from(new Set([...tracked, ...untracked])); diff --git a/packages/cli/src/lib/monorepo/PackageGraph.test.ts b/packages/cli/src/lib/monorepo/PackageGraph.test.ts index 06427a7313..feb1709bf7 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.test.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.test.ts @@ -87,6 +87,11 @@ describe('PackageGraph', () => { localDependencies: new Map([['b', b]]), localDevDependencies: new Map([['c', c]]), localOptionalDependencies: new Map(), + allLocalDependents: new Map(), + publishedLocalDependents: new Map(), + localDependents: new Map(), + localDevDependents: new Map(), + localOptionalDependents: new Map(), }); expect(b).toMatchObject({ name: 'b', @@ -96,6 +101,11 @@ describe('PackageGraph', () => { localDependencies: new Map(), localDevDependencies: new Map([['c', c]]), localOptionalDependencies: new Map(), + allLocalDependents: new Map([['a', a]]), + publishedLocalDependents: new Map([['a', a]]), + localDependents: new Map([['a', a]]), + localDevDependents: new Map(), + localOptionalDependents: new Map(), }); expect(c).toMatchObject({ name: 'c', @@ -105,6 +115,17 @@ describe('PackageGraph', () => { localDependencies: new Map(), localDevDependencies: new Map(), localOptionalDependencies: new Map(), + allLocalDependents: new Map([ + ['a', a], + ['b', b], + ]), + publishedLocalDependents: new Map(), + localDependents: new Map(), + localDevDependents: new Map([ + ['a', a], + ['b', b], + ]), + localOptionalDependents: new Map(), }); }); diff --git a/packages/cli/src/lib/monorepo/PackageGraph.ts b/packages/cli/src/lib/monorepo/PackageGraph.ts index 4bbb9eac67..c65f5c003c 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.ts @@ -47,6 +47,7 @@ export type PackageGraphNode = { dir: string; /** The package data of the package itself */ packageJson: ExtendedPackageJSON; + /** All direct local dependencies of the package */ allLocalDependencies: Map; /** All direct local dependencies that will be present in the published package */ @@ -57,6 +58,17 @@ export type PackageGraphNode = { localDevDependencies: Map; /** Local optionalDependencies */ localOptionalDependencies: Map; + + /** All direct incoming local dependencies of the package */ + allLocalDependents: Map; + /** All direct incoming local dependencies that will be present in the published package */ + publishedLocalDependents: Map; + /** Incoming local dependencies */ + localDependents: Map; + /** Incoming local devDependencies */ + localDevDependents: Map; + /** Incoming local optionalDependencies */ + localOptionalDependents: Map; }; export class PackageGraph extends Map { @@ -82,11 +94,18 @@ export class PackageGraph extends Map { name, dir: pkg.dir, packageJson: pkg.packageJson as ExtendedPackageJSON, + allLocalDependencies: new Map(), publishedLocalDependencies: new Map(), localDependencies: new Map(), localDevDependencies: new Map(), localOptionalDependencies: new Map(), + + allLocalDependents: new Map(), + publishedLocalDependents: new Map(), + localDependents: new Map(), + localDevDependents: new Map(), + localOptionalDependents: new Map(), }); } @@ -98,6 +117,10 @@ export class PackageGraph extends Map { node.allLocalDependencies.set(depName, depPkg); node.publishedLocalDependencies.set(depName, depPkg); node.localDependencies.set(depName, depPkg); + + depPkg.allLocalDependents.set(node.name, node); + depPkg.publishedLocalDependents.set(node.name, node); + depPkg.localDependents.set(node.name, node); } } for (const depName of Object.keys( @@ -107,6 +130,9 @@ export class PackageGraph extends Map { if (depPkg) { node.allLocalDependencies.set(depName, depPkg); node.localDevDependencies.set(depName, depPkg); + + depPkg.allLocalDependents.set(node.name, node); + depPkg.localDevDependents.set(node.name, node); } } for (const depName of Object.keys( @@ -117,6 +143,10 @@ export class PackageGraph extends Map { node.allLocalDependencies.set(depName, depPkg); node.publishedLocalDependencies.set(depName, depPkg); node.localOptionalDependencies.set(depName, depPkg); + + depPkg.allLocalDependents.set(node.name, node); + depPkg.publishedLocalDependents.set(node.name, node); + depPkg.localOptionalDependents.set(node.name, node); } } } diff --git a/packages/cli/src/lib/packager/copyPackageDist.ts b/packages/cli/src/lib/packager/copyPackageDist.ts index 03ac361006..439b087fd7 100644 --- a/packages/cli/src/lib/packager/copyPackageDist.ts +++ b/packages/cli/src/lib/packager/copyPackageDist.ts @@ -20,6 +20,11 @@ import { join as joinPath, resolve as resolvePath } from 'path'; const SKIPPED_KEYS = ['access', 'registry', 'tag', 'alphaTypes', 'betaTypes']; +function resolveEntrypoint(pkg: any, name: string) { + const targetEntry = pkg.publishConfig[name] || pkg[name]; + return targetEntry && joinPath('..', targetEntry); +} + // Writes e.g. alpha/package.json async function writeReleaseStageEntrypoint( pkg: any, @@ -32,9 +37,9 @@ async function writeReleaseStageEntrypoint( { name: pkg.name, version: pkg.version, - main: (pkg.publishConfig.main || pkg.main) && '..', - module: (pkg.publishConfig.module || pkg.module) && '..', - browser: (pkg.publishConfig.browser || pkg.browser) && '..', + main: resolveEntrypoint(pkg, 'main'), + module: resolveEntrypoint(pkg, 'module'), + browser: resolveEntrypoint(pkg, 'browser'), types: joinPath('..', pkg.publishConfig[`${stage}Types`]), }, { encoding: 'utf8', spaces: 2 }, diff --git a/packages/cli/templates/default-backend-plugin/.eslintrc.js b/packages/cli/templates/default-backend-plugin/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/cli/templates/default-backend-plugin/.eslintrc.js +++ b/packages/cli/templates/default-backend-plugin/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 321ca9c546..dacc1d601a 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -15,14 +15,17 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "scripts": { - "start": "backstage-cli backend:dev", - "build": "backstage-cli backend:build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" }, "dependencies": { "@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}", diff --git a/packages/cli/templates/default-common-plugin-package/.eslintrc.js b/packages/cli/templates/default-common-plugin-package/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/cli/templates/default-common-plugin-package/.eslintrc.js +++ b/packages/cli/templates/default-common-plugin-package/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli/templates/default-common-plugin-package/package.json.hbs b/packages/cli/templates/default-common-plugin-package/package.json.hbs index efaee496e7..844a951d05 100644 --- a/packages/cli/templates/default-common-plugin-package/package.json.hbs +++ b/packages/cli/templates/default-common-plugin-package/package.json.hbs @@ -17,13 +17,16 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "common-library" + }, "scripts": { - "build": "backstage-cli build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" }, "devDependencies": { "@backstage/cli": "{{versionQuery '@backstage/cli'}}" diff --git a/packages/cli/templates/default-plugin/.eslintrc.js b/packages/cli/templates/default-plugin/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/cli/templates/default-plugin/.eslintrc.js +++ b/packages/cli/templates/default-plugin/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index d38a31fa7d..f7985b497e 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -15,15 +15,17 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" }, "dependencies": { "@backstage/core-components": "{{versionQuery '@backstage/core-components'}}", diff --git a/packages/cli/templates/scaffolder-module/.eslintrc.js b/packages/cli/templates/scaffolder-module/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/cli/templates/scaffolder-module/.eslintrc.js +++ b/packages/cli/templates/scaffolder-module/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli/templates/scaffolder-module/package.json.hbs b/packages/cli/templates/scaffolder-module/package.json.hbs index de6ffd9e3a..618e953b74 100644 --- a/packages/cli/templates/scaffolder-module/package.json.hbs +++ b/packages/cli/templates/scaffolder-module/package.json.hbs @@ -16,13 +16,17 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin-module" + }, "scripts": { - "build": "backstage-cli build --output cjs,types", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" }, "dependencies": { "@backstage/plugin-scaffolder-backend": "{{versionQuery '@backstage/plugin-scaffolder-backend'}}" diff --git a/packages/codemods/.eslintrc.js b/packages/codemods/.eslintrc.js index 7c8a092081..f9fb2e27c9 100644 --- a/packages/codemods/.eslintrc.js +++ b/packages/codemods/.eslintrc.js @@ -1,5 +1,4 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { 'no-console': 0, 'import/no-extraneous-dependencies': [ @@ -12,4 +11,4 @@ module.exports = { }, ], }, -}; +}); diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 603fa1075e..5bf6535a62 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/codemods +## 0.1.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.1-next.0 + ## 0.1.35 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index c4d2eedb4d..1c863db6cf 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.34", + "version": "0.1.35-next.0", "private": false, "publishConfig": { "access": "public", @@ -36,7 +36,7 @@ "dependencies": { "@backstage/cli-common": "^0.1.8", "@backstage/core-app-api": "*", - "@backstage/core-components": "*", + "@backstage/core-components": "0.9.1-next.0", "@backstage/core-plugin-api": "*", "chalk": "^4.0.0", "jscodeshift": "^0.13.0", diff --git a/packages/config-loader/.eslintrc.js b/packages/config-loader/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/config-loader/.eslintrc.js +++ b/packages/config-loader/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index d9bd7be8ba..362edc78cf 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/config-loader +## 0.9.7-next.0 + +### Patch Changes + +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 + ## 0.9.6 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index d9c9b551b4..71a0e4e650 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.9.6", + "version": "0.9.7-next.0", "private": false, "publishConfig": { "access": "public", @@ -41,7 +41,7 @@ "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", "chokidar": "^3.5.2", - "fs-extra": "9.1.0", + "fs-extra": "10.0.1", "json-schema": "^0.4.0", "json-schema-merge-allof": "^0.8.1", "json-schema-traverse": "^1.0.0", diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index a05796139b..3ff161cec4 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -182,7 +182,7 @@ function compileTsSchemas(paths: string[]) { program, // All schemas should export a `Config` symbol 'Config', - // This enables usage of @visibility and @deprecated in doc comments + // This enables the use of these tags in TSDoc comments { required: true, validationKeywords: ['visibility', 'deprecated'], diff --git a/packages/config/.eslintrc.js b/packages/config/.eslintrc.js index 54e1fff915..e358722664 100644 --- a/packages/config/.eslintrc.js +++ b/packages/config/.eslintrc.js @@ -1,6 +1,5 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { 'jest/expect-expect': 0, }, -}; +}); diff --git a/packages/config/package.json b/packages/config/package.json index 63edfe0360..6b6bac4d3c 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -37,7 +37,7 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/test-utils": "^0.2.5", + "@backstage/test-utils": "^0.3.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/core-app-api/.eslintrc.js b/packages/core-app-api/.eslintrc.js index d592a653c8..e358722664 100644 --- a/packages/core-app-api/.eslintrc.js +++ b/packages/core-app-api/.eslintrc.js @@ -1,8 +1,5 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { - // TODO: add prop types to JS and remove - 'react/prop-types': 0, 'jest/expect-expect': 0, }, -}; +}); diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index f40ea23f73..f286c3d787 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/core-app-api +## 0.6.0 + +### Minor Changes + +- bb2bb36651: **BREAKING**: Removed the deprecated `get` method from `StorageAPI` and its implementations, this method has been replaced by the `snapshot` method. The return value from snapshot no longer includes `newValue` which has been replaced by `value`. For getting notified when a value changes, use `observe# @backstage/core-app-api. +- f3cce3dcf7: **BREAKING**: Removed export of `GithubSession` and `SamlSession` which are only used internally. +- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). +- dbf84eee55: **BREAKING**: Removed the deprecated `GithubAuth.normalizeScopes` method. + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@0.8.0 + ## 0.5.4 ### Patch Changes diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 3b53813615..116b33c598 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -16,7 +16,6 @@ import { AppConfig } from '@backstage/config'; import { AppTheme } from '@backstage/core-plugin-api'; import { AppThemeApi } from '@backstage/core-plugin-api'; import { atlassianAuthApiRef } from '@backstage/core-plugin-api'; -import { auth0AuthApiRef } from '@backstage/core-plugin-api'; import { AuthProviderInfo } from '@backstage/core-plugin-api'; import { AuthRequestOptions } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; @@ -246,12 +245,6 @@ export class AtlassianAuth { static create(options: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T; } -// @public @deprecated -export class Auth0Auth { - // (undocumented) - static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T; -} - // @public export type AuthApiCreateOptions = { discoveryApi: DiscoveryApi; @@ -383,21 +376,8 @@ export type FlatRoutesProps = { export class GithubAuth { // (undocumented) static create(options: OAuthApiCreateOptions): typeof githubAuthApiRef.T; - // @deprecated (undocumented) - static normalizeScope(scope?: string): Set; } -// @public @deprecated -export type GithubSession = { - providerInfo: { - accessToken: string; - scopes: Set; - expiresAt?: Date; - }; - profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; -}; - // @public export class GitlabAuth { // (undocumented) @@ -539,13 +519,6 @@ export class SamlAuth signOut(): Promise; } -// @public @deprecated -export type SamlSession = { - userId: string; - profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; -}; - // @public export type SignInPageProps = { onSignInSuccess(identityApi: IdentityApi): void; diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 6f74bf95d6..f0776fcb85 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.5.4", + "version": "0.6.0", "private": false, "publishConfig": { "access": "public", @@ -34,7 +34,7 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/types": "^0.1.3", "@backstage/version-bridge": "^0.1.2", "@types/prop-types": "^15.7.3", @@ -49,8 +49,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts index d81cba48d2..2f82ed5e88 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -33,12 +33,11 @@ describe('WebStorage Storage API', () => { it('should return undefined for values which are unset', async () => { const storage = createWebStorage(); - expect(storage.get('myfakekey')).toBeUndefined(); + expect(storage.snapshot('myfakekey').value).toBeUndefined(); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'absent', value: undefined, - newValue: undefined, }); }); @@ -48,26 +47,23 @@ describe('WebStorage Storage API', () => { await storage.set('myfakekey', 'helloimastring'); await storage.set('mysecondfakekey', 1234); await storage.set('mythirdfakekey', true); - expect(storage.get('myfakekey')).toBe('helloimastring'); - expect(storage.get('mysecondfakekey')).toBe(1234); - expect(storage.get('mythirdfakekey')).toBe(true); + expect(storage.snapshot('myfakekey').value).toBe('helloimastring'); + expect(storage.snapshot('mysecondfakekey').value).toBe(1234); + expect(storage.snapshot('mythirdfakekey').value).toBe(true); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'present', value: 'helloimastring', - newValue: 'helloimastring', }); expect(storage.snapshot('mysecondfakekey')).toEqual({ key: 'mysecondfakekey', presence: 'present', value: 1234, - newValue: 1234, }); expect(storage.snapshot('mythirdfakekey')).toEqual({ key: 'mythirdfakekey', presence: 'present', value: true, - newValue: true, }); }); @@ -81,12 +77,11 @@ describe('WebStorage Storage API', () => { await storage.set('myfakekey', mockData); - expect(storage.get('myfakekey')).toEqual(mockData); + expect(storage.snapshot('myfakekey').value).toEqual(mockData); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'present', value: mockData, - newValue: mockData, }); }); @@ -118,7 +113,6 @@ describe('WebStorage Storage API', () => { key: 'correctKey', presence: 'present', value: mockData, - newValue: mockData, }); }); @@ -152,7 +146,6 @@ describe('WebStorage Storage API', () => { key: 'correctKey', presence: 'absent', value: undefined, - newValue: undefined, }); }); @@ -166,9 +159,11 @@ describe('WebStorage Storage API', () => { await firstStorage.set(keyName, 'boop'); await secondStorage.set(keyName, 'deerp'); - expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName)); - expect(firstStorage.get(keyName)).toBe('boop'); - expect(secondStorage.get(keyName)).toBe('deerp'); + expect(firstStorage.snapshot(keyName)).not.toBe( + secondStorage.snapshot(keyName), + ); + expect(firstStorage.snapshot(keyName).value).toBe('boop'); + expect(secondStorage.snapshot(keyName).value).toBe('deerp'); expect(firstStorage.snapshot(keyName)).not.toEqual( secondStorage.snapshot(keyName), ); @@ -176,13 +171,11 @@ describe('WebStorage Storage API', () => { key: keyName, presence: 'present', value: 'boop', - newValue: 'boop', }); expect(secondStorage.snapshot(keyName)).toEqual({ key: keyName, presence: 'present', value: 'deerp', - newValue: 'deerp', }); }); @@ -217,7 +210,6 @@ describe('WebStorage Storage API', () => { key: 'key', presence: 'absent', value: undefined, - newValue: undefined, }); expect(mockErrorApi.post).toHaveBeenCalledWith(expect.any(Error)); expect(mockErrorApi.post).toHaveBeenCalledWith( diff --git a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts index c4cffaf184..ac3d3f20f2 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -65,7 +65,7 @@ export class WebStorage implements StorageApi { new Error(`Error when parsing JSON config from storage for: ${key}`), ); } - return { key, value, newValue: value, presence }; + return { key, value, presence }; } forBucket(name: string): WebStorage { diff --git a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts deleted file mode 100644 index d8f942b94b..0000000000 --- a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts +++ /dev/null @@ -1,67 +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 { auth0AuthApiRef } from '@backstage/core-plugin-api'; -import { OAuth2 } from '../oauth2'; -import { OAuthApiCreateOptions } from '../types'; - -const DEFAULT_PROVIDER = { - id: 'auth0', - title: 'Auth0', - icon: () => null, -}; - -/** - * Implements the OAuth flow to Auth0 products. - * - * @public - * @deprecated Use {@link OAuth2} instead - * - * @example - * - * ```ts - * OAuth2.create({ - * discoveryApi, - * oauthRequestApi, - * provider: { - * id: 'auth0', - * title: 'Auth0', - * icon: () => null, - * }, - * defaultScopes: ['openid', 'email', 'profile'], - * environment: configApi.getOptionalString('auth.environment'), - * }) - * ``` - */ -export default class Auth0Auth { - static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T { - const { - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['openid', `email`, `profile`], - } = options; - - return OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider, - environment, - defaultScopes, - }); - } -} diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts index b0af4c7ade..7efe4e95c6 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -47,19 +47,4 @@ export default class GithubAuth { defaultScopes, }); } - - /** - * @deprecated This method is deprecated and will be removed in a future release. - */ - static normalizeScope(scope?: string): Set { - if (!scope) { - return new Set(); - } - - const scopeList = Array.isArray(scope) - ? scope - : scope.split(/[\s|,]/).filter(Boolean); - - return new Set(scopeList); - } } diff --git a/packages/core-app-api/src/apis/implementations/auth/github/index.ts b/packages/core-app-api/src/apis/implementations/auth/github/index.ts index b5aa1a0a25..5e53bacfc1 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export type { GithubSession } from './types'; export { default as GithubAuth } from './GithubAuth'; diff --git a/packages/core-app-api/src/apis/implementations/auth/github/types.ts b/packages/core-app-api/src/apis/implementations/auth/github/types.ts deleted file mode 100644 index 0ef662905f..0000000000 --- a/packages/core-app-api/src/apis/implementations/auth/github/types.ts +++ /dev/null @@ -1,62 +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 { - ProfileInfo, - BackstageIdentityResponse, -} from '@backstage/core-plugin-api'; -import { z } from 'zod'; - -// TODO(Rugvip): Make GithubSession internal - -/** - * Session information for GitHub auth. - * - * @public - * @deprecated This type is internal and will be removed - */ -export type GithubSession = { - providerInfo: { - accessToken: string; - scopes: Set; - expiresAt?: Date; - }; - profile: ProfileInfo; - // TODO(Rugvip): This should be made optional once the type is no longer public - backstageIdentity: BackstageIdentityResponse; -}; - -export const githubSessionSchema: z.ZodSchema = z.object({ - providerInfo: z.object({ - accessToken: z.string(), - scopes: z.set(z.string()), - expiresAt: z.date().optional(), - }), - profile: z.object({ - email: z.string().optional(), - displayName: z.string().optional(), - picture: z.string().optional(), - }), - backstageIdentity: z.object({ - id: z.string(), - token: z.string(), - identity: z.object({ - type: z.literal('user'), - userEntityRef: z.string(), - ownershipEntityRefs: z.array(z.string()), - }), - }), -}); diff --git a/packages/core-app-api/src/apis/implementations/auth/index.ts b/packages/core-app-api/src/apis/implementations/auth/index.ts index 50333f07a0..c4cf520db5 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -20,7 +20,6 @@ export * from './google'; export * from './oauth2'; export * from './okta'; export * from './saml'; -export * from './auth0'; export * from './microsoft'; export * from './onelogin'; export * from './bitbucket'; diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/index.ts b/packages/core-app-api/src/apis/implementations/auth/saml/index.ts index 2e749a0648..930e6cb115 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/index.ts @@ -14,4 +14,3 @@ * limitations under the License. */ export { default as SamlAuth } from './SamlAuth'; -export type { ExportedSamlSession as SamlSession } from './types'; diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts index f1345fd154..55b278b429 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts @@ -20,18 +20,6 @@ import { } from '@backstage/core-plugin-api'; import { z } from 'zod'; -/** - * Session information for SAML auth. - * - * @public - * @deprecated This type is internal and will be removed - */ -export type ExportedSamlSession = { - userId: string; - profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; -}; - /** @internal */ export type SamlSession = { profile: ProfileInfo; diff --git a/packages/core-components/.eslintrc.js b/packages/core-components/.eslintrc.js index 65a93d3660..2ebb50e64f 100644 --- a/packages/core-components/.eslintrc.js +++ b/packages/core-components/.eslintrc.js @@ -1,25 +1,11 @@ -const base = require('@backstage/cli/config/eslint'); -const [, baseRestrictedImports] = base.rules['no-restricted-imports']; - -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { - // TODO: add prop types to JS and remove - 'react/prop-types': 0, 'jest/expect-expect': 0, - 'no-restricted-imports': [ - 2, - { - ...baseRestrictedImports, - paths: [ - { - // Importing the entire MUI icons packages kills build performance as the list of icons is huge. - name: '@material-ui/core', - message: "Please import '@material-ui/core/...' instead.", - }, - ...baseRestrictedImports.paths, - ], - }, - ], }, -}; + restrictedImports: [ + { + name: '@material-ui/core', + message: "Please import '@material-ui/core/...' instead.", + }, + ], +}); diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 5d8211d303..64a3d0614d 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/core-components +## 0.9.1-next.0 + +### Patch Changes + +- 23568dd328: chore(deps): bump `@react-hookz/web` from 12.3.0 to 13.0.0 +- 95667624c1: Add names to sidebar sub menu styles for customization + +## 0.9.0 + +### Minor Changes + +- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). + +### Patch Changes + +- 64b430f80d: chore(deps): bump `react-text-truncate` from 0.17.0 to 0.18.0 +- bb2bb36651: Updated usage of `StorageApi` to use `snapshot` method instead of `get` +- 689840dcbe: Added ability for SidebarSubmenuItem to handle external links correctly via the "to" prop +- Updated dependencies + - @backstage/core-plugin-api@0.8.0 + ## 0.8.10 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 87d9f78fcc..d71bcff3cf 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.8.10", + "version": "0.9.1-next.0", "private": false, "publishConfig": { "access": "public", @@ -34,7 +34,7 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", "@backstage/theme": "^0.2.15", "@material-table/core": "^3.1.0", @@ -65,7 +65,7 @@ "react-syntax-highlighter": "^15.4.5", "react-text-truncate": "^0.18.0", "react-use": "^17.3.2", - "@react-hookz/web": "^12.3.0", + "@react-hookz/web": "^13.0.0", "react-virtualized-auto-sizer": "^1.0.6", "react-window": "^1.8.6", "remark-gfm": "^3.0.1", @@ -78,9 +78,9 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/core-app-api": "^0.5.4", - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/core-app-api": "^0.6.0", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index 49f209d04e..6db8e2dd02 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -99,7 +99,7 @@ export interface DependencyGraphProps /** * Margin on top and bottom of whole graph * - * @remarks + * @remarks * * Default: 0 */ diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx index e62b3fa82d..3348c278ee 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx @@ -75,7 +75,7 @@ describe('', () => { ); fireEvent.click(button); const dismissedBanners = - notifications?.get('dismissedBanners') ?? []; + notifications?.snapshot('dismissedBanners').value ?? []; expect( dismissedBanners.includes('catalog_page_welcome_banner'), ).toBeTruthy(); diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx index 566b326cb5..9ff7616c45 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx @@ -101,7 +101,7 @@ export const DismissableBanner = (props: Props) => { const storageApi = useApi(storageApiRef); const notificationsStore = storageApi.forBucket('notifications'); const rawDismissedBanners = - notificationsStore.get('dismissedBanners') ?? []; + notificationsStore.snapshot('dismissedBanners').value ?? []; const [dismissedBanners, setDismissedBanners] = useState( new Set(rawDismissedBanners), @@ -112,11 +112,11 @@ export const DismissableBanner = (props: Props) => { ); useEffect(() => { - if (observedItems?.newValue) { - const currentValue = observedItems?.newValue ?? []; + if (observedItems?.value) { + const currentValue = observedItems?.value ?? []; setDismissedBanners(new Set(currentValue)); } - }, [observedItems?.newValue]); + }, [observedItems?.value]); const handleClick = () => { notificationsStore.set('dismissedBanners', [...dismissedBanners, id]); diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx index 60175e2439..5a511b2094 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx @@ -26,62 +26,65 @@ import { import { BackstageTheme } from '@backstage/theme'; const useStyles = (props: { left: number }) => - makeStyles(theme => ({ - root: { - zIndex: 1000, - position: 'relative', - overflow: 'visible', - width: theme.spacing(7) + 1, - }, - drawer: { - display: 'flex', - flexFlow: 'column nowrap', - alignItems: 'flex-start', - position: 'fixed', - [theme.breakpoints.up('sm')]: { - marginLeft: props.left, - transition: theme.transitions.create('margin-left', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.shortest, - }), - }, - top: 0, - bottom: 0, - padding: 0, - background: theme.palette.navigation.submenu?.background ?? '#404040', - overflowX: 'hidden', - msOverflowStyle: 'none', - scrollbarWidth: 'none', - cursor: 'default', - width: submenuConfig.drawerWidthClosed, - transitionDelay: `${submenuConfig.defaultOpenDelayMs}ms`, - '& > *': { - flexShrink: 0, - }, - '&::-webkit-scrollbar': { - display: 'none', - }, - }, - drawerOpen: { - width: submenuConfig.drawerWidthOpen, - [theme.breakpoints.down('xs')]: { - width: '100%', + makeStyles( + theme => ({ + root: { + zIndex: 1000, position: 'relative', - paddingLeft: theme.spacing(3), - left: 0, + overflow: 'visible', + width: theme.spacing(7) + 1, + }, + drawer: { + display: 'flex', + flexFlow: 'column nowrap', + alignItems: 'flex-start', + position: 'fixed', + [theme.breakpoints.up('sm')]: { + marginLeft: props.left, + transition: theme.transitions.create('margin-left', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.shortest, + }), + }, top: 0, + bottom: 0, + padding: 0, + background: theme.palette.navigation.submenu?.background ?? '#404040', + overflowX: 'hidden', + msOverflowStyle: 'none', + scrollbarWidth: 'none', + cursor: 'default', + width: submenuConfig.drawerWidthClosed, + transitionDelay: `${submenuConfig.defaultOpenDelayMs}ms`, + '& > *': { + flexShrink: 0, + }, + '&::-webkit-scrollbar': { + display: 'none', + }, }, - }, - title: { - fontSize: 24, - fontWeight: 500, - color: '#FFF', - padding: 20, - [theme.breakpoints.down('xs')]: { - display: 'none', + drawerOpen: { + width: submenuConfig.drawerWidthOpen, + [theme.breakpoints.down('xs')]: { + width: '100%', + position: 'relative', + paddingLeft: theme.spacing(3), + left: 0, + top: 0, + }, }, - }, - })); + title: { + fontSize: 24, + fontWeight: 500, + color: '#FFF', + padding: 20, + [theme.breakpoints.down('xs')]: { + display: 'none', + }, + }, + }), + { name: 'BackstageSidebarSubmenu' }, + ); /** * Holds a title for text Header of a sidebar submenu and children diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx index d204728bcf..5c9dce010d 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx @@ -26,59 +26,62 @@ import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp'; import { SidebarItemWithSubmenuContext } from './config'; import { isLocationMatch } from './utils'; -const useStyles = makeStyles(theme => ({ - item: { - height: 48, - width: '100%', - '&:hover': { +const useStyles = makeStyles( + theme => ({ + item: { + height: 48, + width: '100%', + '&:hover': { + background: '#6f6f6f', + color: theme.palette.navigation.selectedColor, + }, + display: 'flex', + alignItems: 'center', + color: theme.palette.navigation.color, + padding: 20, + cursor: 'pointer', + position: 'relative', + background: 'none', + border: 'none', + }, + itemContainer: { + width: '100%', + }, + selected: { background: '#6f6f6f', - color: theme.palette.navigation.selectedColor, + color: '#FFF', }, - display: 'flex', - alignItems: 'center', - color: theme.palette.navigation.color, - padding: 20, - cursor: 'pointer', - position: 'relative', - background: 'none', - border: 'none', - }, - itemContainer: { - width: '100%', - }, - selected: { - background: '#6f6f6f', - color: '#FFF', - }, - label: { - margin: 14, - marginLeft: 7, - fontSize: 14, - }, - dropdownArrow: { - position: 'absolute', - right: 21, - }, - dropdown: { - display: 'flex', - flexDirection: 'column', - alignItems: 'end', - }, - dropdownItem: { - width: '100%', - padding: '10px 0 10px 0', - }, - textContent: { - color: theme.palette.navigation.color, - display: 'flex', - justifyContent: 'center', - [theme.breakpoints.down('xs')]: { - display: 'block', - paddingLeft: theme.spacing(4), + label: { + margin: 14, + marginLeft: 7, + fontSize: 14, }, - fontSize: '14px', - }, -})); + dropdownArrow: { + position: 'absolute', + right: 21, + }, + dropdown: { + display: 'flex', + flexDirection: 'column', + alignItems: 'end', + }, + dropdownItem: { + width: '100%', + padding: '10px 0 10px 0', + }, + textContent: { + color: theme.palette.navigation.color, + display: 'flex', + justifyContent: 'center', + [theme.breakpoints.down('xs')]: { + display: 'block', + paddingLeft: theme.spacing(4), + }, + fontSize: '14px', + }, + }), + { name: 'BackstageSidebarSubmenuItem' }, +); /** * Clickable item displayed when submenu item is clicked. diff --git a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx b/packages/core-components/src/layout/SignInPage/auth0Provider.tsx deleted file mode 100644 index 739c3709a9..0000000000 --- a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx +++ /dev/null @@ -1,95 +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 React from 'react'; -import Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; -import { InfoCard } from '../InfoCard/InfoCard'; -import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; -import { - useApi, - auth0AuthApiRef, - errorApiRef, -} from '@backstage/core-plugin-api'; -import { ForwardedError } from '@backstage/errors'; -import { UserIdentity } from './UserIdentity'; - -const Component: ProviderComponent = ({ onSignInSuccess }) => { - const auth0AuthApi = useApi(auth0AuthApiRef); - const errorApi = useApi(errorApiRef); - - const handleLogin = async () => { - try { - const identityResponse = await auth0AuthApi.getBackstageIdentity({ - instantPopup: true, - }); - if (!identityResponse) { - throw new Error( - 'The Auth0 provider is not configured to support sign-in', - ); - } - - const profile = await auth0AuthApi.getProfile(); - - onSignInSuccess( - UserIdentity.create({ - identity: identityResponse.identity, - authApi: auth0AuthApi, - profile, - }), - ); - } catch (error) { - errorApi.post(new ForwardedError('Auth0 login failed', error)); - } - }; - - return ( - - - Sign In - - } - > - Sign In using Auth0 - - - ); -}; - -const loader: ProviderLoader = async apis => { - const auth0AuthApi = apis.get(auth0AuthApiRef)!; - - const identityResponse = await auth0AuthApi.getBackstageIdentity({ - optional: true, - }); - - if (!identityResponse) { - return undefined; - } - - const profile = await auth0AuthApi.getProfile(); - return UserIdentity.create({ - identity: identityResponse.identity, - authApi: auth0AuthApi, - profile, - }); -}; - -export const auth0Provider: SignInProvider = { Component, loader }; diff --git a/packages/core-plugin-api/.eslintrc.js b/packages/core-plugin-api/.eslintrc.js index d592a653c8..e2a53a6ad2 100644 --- a/packages/core-plugin-api/.eslintrc.js +++ b/packages/core-plugin-api/.eslintrc.js @@ -1,8 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], - rules: { - // TODO: add prop types to JS and remove - 'react/prop-types': 0, - 'jest/expect-expect': 0, - }, -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index bba39bf2cf..3702de3474 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/core-plugin-api +## 0.8.0 + +### Minor Changes + +- bb2bb36651: **BREAKING**: Removed the deprecated `get` method from `StorageAPI` and its implementations, this method has been replaced by the `snapshot` method. The return value from snapshot no longer includes `newValue` which has been replaced by `value`. For getting notified when a value changes, use `observe# @backstage/core-plugin-api. +- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). +- a480f670c7: **BREAKING**: OAuth provider id is now required when passing a provider to `createAuthRequester`. + ## 0.7.0 ### Minor Changes diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 8c49deec2b..89361419ba 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -184,11 +184,6 @@ export function attachComponentData

( data: unknown, ): void; -// @public @deprecated -export const auth0AuthApiRef: ApiRef< - OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; - // @public export type AuthProviderInfo = { id: string; @@ -531,15 +526,6 @@ export const microsoftAuthApiRef: ApiRef< SessionApi >; -// @public @deprecated -export const oauth2ApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; - // @public export type OAuthApi = { getAccessToken( @@ -573,15 +559,6 @@ export type OAuthRequesterOptions = { // @public export type OAuthScope = string | string[]; -// @public @deprecated -export const oidcAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; - // @alpha export const oktaAuthApiRef: ApiRef< OAuthApi & @@ -680,11 +657,6 @@ export type RouteRef = { params: ParamKeys; }; -// @public @deprecated -export const samlAuthApiRef: ApiRef< - ProfileInfoApi & BackstageIdentityApi & SessionApi ->; - // @public export type SessionApi = { signIn(): Promise; @@ -706,8 +678,6 @@ export type SignInPageProps = { // @public export interface StorageApi { forBucket(name: string): StorageApi; - // @deprecated - get(key: string): T | undefined; observe$( key: string, ): Observable>; @@ -719,23 +689,17 @@ export interface StorageApi { // @public export const storageApiRef: ApiRef; -// @public @deprecated (undocumented) -export type StorageValueChange = - StorageValueSnapshot; - // @public export type StorageValueSnapshot = | { key: string; presence: 'unknown' | 'absent'; value?: undefined; - newValue?: undefined; } | { key: string; presence: 'present'; value: TValue; - newValue?: TValue; }; // @public diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 4e9198557b..dd7056c8de 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "0.7.0", + "version": "0.8.0", "private": false, "publishConfig": { "access": "public", @@ -46,9 +46,9 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts index 4bd49f9b83..1506e5f143 100644 --- a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts @@ -27,24 +27,13 @@ export type StorageValueSnapshot = key: string; presence: 'unknown' | 'absent'; value?: undefined; - /** @deprecated Use `value` instead */ - newValue?: undefined; } | { key: string; presence: 'present'; value: TValue; - /** @deprecated Use `value` instead */ - newValue?: TValue; }; -/** - * @public - * @deprecated Use StorageValueSnapshot instead - */ -export type StorageValueChange = - StorageValueSnapshot; - /** * Provides a key-value persistence API. * @@ -59,14 +48,6 @@ export interface StorageApi { */ forBucket(name: string): StorageApi; - /** - * Get the current value for persistent data, use observe$ to be notified of updates. - * - * @deprecated Use `snapshot` instead. - * @param key - Unique key associated with the data. - */ - get(key: string): T | undefined; - /** * Remove persistent data. * diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index ed5a7a3264..497ce8233b 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -366,23 +366,6 @@ export const gitlabAuthApiRef: ApiRef< id: 'core.auth.gitlab', }); -/** - * Provides authentication towards Auth0 APIs. - * - * @remarks - * - * See {@link https://auth0.com/docs/scopes/current/oidc-scopes} - * for a full list of supported scopes. - * - * @public - * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs - */ -export const auth0AuthApiRef: ApiRef< - OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.auth0', -}); - /** * Provides authentication towards Microsoft APIs and identities. * @@ -404,50 +387,6 @@ export const microsoftAuthApiRef: ApiRef< id: 'core.auth.microsoft', }); -/** - * Provides authentication for custom identity providers. - * - * @public - * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs - */ -export const oauth2ApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.oauth2', -}); - -/** - * Provides authentication for custom OpenID Connect identity providers. - * - * @public - * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs - */ -export const oidcAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.oidc', -}); - -/** - * Provides authentication for SAML-based identity providers. - * - * @public - * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs - */ -export const samlAuthApiRef: ApiRef< - ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.saml', -}); - /** * Provides authentication towards OneLogin APIs. * diff --git a/packages/create-app/.eslintrc.js b/packages/create-app/.eslintrc.js index 69bec6cd2a..e9ea2f5bfb 100644 --- a/packages/create-app/.eslintrc.js +++ b/packages/create-app/.eslintrc.js @@ -1,7 +1,6 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { ignorePatterns: ['templates/**'], rules: { 'no-console': 0, }, -}; +}); diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 52dad1bfc9..c9b83a1157 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,154 @@ # @backstage/create-app +## 0.4.23-next.0 + +### Patch Changes + +- f9c7bdd899: Builtin support for cookiecutter based templates has been removed from `@backstage/plugin-scaffolder-backend`. Due to this, the `containerRunner` argument to its `createRouter` has also been removed. + + If you do not use cookiecutter templates and are fine with removing support from it in your own installation, update your `packages/backend/src/plugins/scaffolder.ts` file as follows: + + ```diff + -import { DockerContainerRunner } from '@backstage/backend-common'; + import { CatalogClient } from '@backstage/catalog-client'; + import { createRouter } from '@backstage/plugin-scaffolder-backend'; + -import Docker from 'dockerode'; + import { Router } from 'express'; + import type { PluginEnvironment } from '../types'; + + export default async function createPlugin({ + reader, + discovery, + }: PluginEnvironment): Promise { + - const dockerClient = new Docker(); + - const containerRunner = new DockerContainerRunner({ dockerClient }); + - + const catalogClient = new CatalogClient({ discoveryApi: discovery }); + - + return await createRouter({ + - containerRunner, + logger, + config, + // ... + ``` + + If you want to retain cookiecutter support, please use the `@backstage/plugin-scaffolder-backend-module-cookiecutter` package explicitly (see [its README](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend-module-cookiecutter) for installation instructions). + +- 8a57b6595b: Removed the `cookiecutter-golang` template from the default `create-app` install as we no longer provide `cookiecutter` action out of the box. + + You can remove the template by removing the following lines from your `app-config.yaml` under `catalog.locations`: + + ```diff + - - type: url + - target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml + - rules: + - - allow: [Template] + ``` + +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- 1201383b60: Updated the template to write the Backstage release version to `backstage.json`, rather than the version of `@backstage/create-app`. This change is applied automatically when running `backstage-cli versions:bump` in the latest version of the Backstage CLI. +- c543fe3ff2: Postgres-based search is now installed when PG is chosen as the desired database for Backstage. + + There is no need to make this change in an existing Backstage backend. See [supported search engines](https://backstage.io/docs/features/search/search-engines) for details about production-ready search engines. + +- 55150919ed: - **BREAKING**: Support for `backstage.io/v1beta2` Software Templates has been removed. Please migrate your legacy templates to the new `scaffolder.backstage.io/v1beta3` `apiVersion` by following the [migration guide](https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3) +- bde30664c4: Updated template to use package roles. To apply this change to an existing app, check out the [migration guide](https://backstage.io/docs/tutorials/package-role-migration). + + Specifically the following scripts in the root `package.json` have also been updated: + + ```diff + - "build": "lerna run build", + + "build": "backstage-cli repo build --all", + + ... + + - "lint": "lerna run lint --since origin/master --", + - "lint:all": "lerna run lint --", + + "lint": "backstage-cli repo lint --since origin/master", + + "lint:all": "backstage-cli repo lint", + ``` + +## 0.4.22 + +### Patch Changes + +- ee3d6c6f10: Update the template to reflect the renaming of `DocsResultListItem` to `TechDocsSearchResultListItem` from `@backstage/plugin-techdocs`. + + To apply this change to an existing app, make the following change to `packages/app/src/components/search/SearchPage.tsx`: + + ```diff + -import { DocsResultListItem } from '@backstage/plugin-techdocs'; + +import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; + ``` + + ```diff + case 'techdocs': + return ( + - + ``` + + The `TechDocsIndexPage` now uses `DefaultTechDocsHome` as fall back if no children is provided as `LegacyTechDocsHome` is marked as deprecated. If you do not use a custom techdocs homepage, you can therefore update your app to the following: + + ```diff + - }> + - + - + + } /> + ``` + +- 617a132871: Update import location of catalogEntityCreatePermission. + + To apply this change to an existing app, make the following change to `packages/app/src/App.tsx`: + + ```diff + -import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; + +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; + ``` + +- 022507c860: The Backstage Search Platform's indexing process has been rewritten as a stream + pipeline in order to improve efficiency and performance on large document sets. + + To take advantage of this, upgrade to the latest version of + `@backstage/plugin-search-backend-node`, as well as any backend plugins whose + collators you are using. Then, make the following changes to your + `/packages/backend/src/plugins/search.ts` file: + + ```diff + -import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; + -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; + +import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; + +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; + + // ... + + const indexBuilder = new IndexBuilder({ logger, searchEngine }); + + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + - collator: DefaultCatalogCollator.fromConfig(config, { discovery }), + + factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery }), + }); + + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + - collator: DefaultTechDocsCollator.fromConfig(config, { + + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { + discovery, + logger, + }), + }); + ``` + + If you've written custom collators, decorators, or search engines in your + Backstage backend instance, you will need to re-implement them as readable, + transform, and writable streams respectively (including factory classes for + instantiating them). [A how-to guide for refactoring](https://backstage.io/docs/features/search/how-to-guides#rewriting-alpha-style-collators-for-beta) + existing implementations is available. + ## 0.4.21 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index a240e95cac..838d54b0f4 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.4.21", + "version": "0.4.23-next.0", "private": false, "publishConfig": { "access": "public" @@ -36,7 +36,7 @@ "@backstage/cli-common": "^0.1.8", "chalk": "^4.0.0", "commander": "^6.1.0", - "fs-extra": "9.1.0", + "fs-extra": "10.0.1", "handlebars": "^4.7.3", "inquirer": "^8.2.0", "ora": "^5.3.0", @@ -86,6 +86,7 @@ "@backstage/plugin-scaffolder-backend": "", "@backstage/plugin-search": "", "@backstage/plugin-search-backend": "", + "@backstage/plugin-search-backend-module-pg": "", "@backstage/plugin-search-backend-node": "", "@backstage/plugin-tech-radar": "", "@backstage/plugin-techdocs": "", diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index ee01e8fd42..09099e2d15 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -59,7 +59,7 @@ describe('command entrypoint', () => { test('should call expected tasks with no path option', async () => { const cmd = {} as unknown as Command; - await createApp(cmd, '1.0.0'); + await createApp(cmd); expect(checkAppExistsMock).toHaveBeenCalled(); expect(createTemporaryAppFolderMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); @@ -69,7 +69,7 @@ describe('command entrypoint', () => { it('should call expected tasks with path option', async () => { const cmd = { path: 'myDirectory' } as unknown as Command; - await createApp(cmd, '1.0.0'); + await createApp(cmd); expect(checkPathExistsMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(buildAppMock).toHaveBeenCalled(); @@ -77,7 +77,7 @@ describe('command entrypoint', () => { it('should not call `buildAppTask` when `skipInstall` is supplied', async () => { const cmd = { skipInstall: true } as unknown as Command; - await createApp(cmd, '1.0.0'); + await createApp(cmd); expect(buildAppMock).not.toHaveBeenCalled(); }); }); diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index d4d30364dc..36143a4172 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -30,7 +30,7 @@ import { templatingTask, } from './lib/tasks'; -export default async (cmd: Command, version: string): Promise => { +export default async (cmd: Command): Promise => { /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); @@ -80,7 +80,7 @@ export default async (cmd: Command, version: string): Promise => { await checkPathExistsTask(appDir); Task.section('Preparing files'); - await templatingTask(templateDir, cmd.path, answers, version); + await templatingTask(templateDir, cmd.path, answers); } else { // Template to temporary location, and then move files @@ -91,7 +91,7 @@ export default async (cmd: Command, version: string): Promise => { await createTemporaryAppFolderTask(tempDir); Task.section('Preparing files'); - await templatingTask(templateDir, tempDir, answers, version); + await templatingTask(templateDir, tempDir, answers); Task.section('Moving to final location'); await moveAppTask(tempDir, appDir, answers.name); diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index 8a2da2fad8..a8ff3dc9b9 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -38,7 +38,7 @@ const main = (argv: string[]) => { '--skip-install', 'Skip the install and builds steps after creating the app', ) - .action(cmd => createApp(cmd, version)); + .action(cmd => createApp(cmd)); program.parse(argv); }; diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index d7b4692aa4..0c92173dbf 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -18,6 +18,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; import child_process from 'child_process'; import path from 'path'; +import { version as releaseVersion } from '../../../../package.json'; import { buildAppTask, checkAppExistsTask, @@ -195,11 +196,11 @@ describe('templatingTask', () => { name: 'SuperCoolBackstageInstance', dbTypeSqlite: true, }; - await templatingTask(templateDir, destinationDir, context, '1.0.0'); + await templatingTask(templateDir, destinationDir, context); expect(fs.existsSync('templatedApp/package.json')).toBe(true); expect(fs.existsSync('templatedApp/.dockerignore')).toBe(true); await expect(fs.readJson('templatedApp/backstage.json')).resolves.toEqual({ - version: '1.0.0', + version: releaseVersion, }); // catalog was populated with `context.name` expect( diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 4bd7050fc2..1650432969 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { BACKSTAGE_JSON } from '@backstage/cli-common'; import chalk from 'chalk'; import fs from 'fs-extra'; import handlebars from 'handlebars'; @@ -23,7 +22,6 @@ import recursive from 'recursive-readdir'; import { basename, dirname, - join, resolve as resolvePath, relative as relativePath, } from 'path'; @@ -86,7 +84,6 @@ export async function templatingTask( templateDir: string, destinationDir: string, context: any, - version: string, ) { const files = await recursive(templateDir).catch(error => { throw new Error(`Failed to read template directory: ${error.message}`); @@ -136,12 +133,6 @@ export async function templatingTask( }); } } - await Task.forItem('creating', BACKSTAGE_JSON, () => - fs.writeFile( - join(destinationDir, BACKSTAGE_JSON), - `{\n "version": ${JSON.stringify(version)}\n}\n`, - ), - ); } /** diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 7179b7924f..963c04afdf 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -30,6 +30,8 @@ Rollup will extract the value of the version field in each package at build time leaving any imports in place. */ +import { version as root } from '../../../../package.json'; + import { version as appDefaults } from '../../../app-defaults/package.json'; import { version as backendCommon } from '../../../backend-common/package.json'; import { version as backendTasks } from '../../../backend-tasks/package.json'; @@ -68,6 +70,7 @@ import { version as pluginScaffolder } from '../../../../plugins/scaffolder/pack import { version as pluginScaffolderBackend } from '../../../../plugins/scaffolder-backend/package.json'; import { version as pluginSearch } from '../../../../plugins/search/package.json'; import { version as pluginSearchBackend } from '../../../../plugins/search-backend/package.json'; +import { version as pluginSearchBackendModulePg } from '../../../../plugins/search-backend-module-pg/package.json'; import { version as pluginSearchBackendNode } from '../../../../plugins/search-backend-node/package.json'; import { version as pluginTechRadar } from '../../../../plugins/tech-radar/package.json'; import { version as pluginTechdocs } from '../../../../plugins/techdocs/package.json'; @@ -75,6 +78,7 @@ import { version as pluginTechdocsBackend } from '../../../../plugins/techdocs-b import { version as pluginUserSettings } from '../../../../plugins/user-settings/package.json'; export const packageVersions = { + root, '@backstage/app-defaults': appDefaults, '@backstage/backend-common': backendCommon, '@backstage/backend-tasks': backendTasks, @@ -110,6 +114,7 @@ export const packageVersions = { '@backstage/plugin-scaffolder-backend': pluginScaffolderBackend, '@backstage/plugin-search': pluginSearch, '@backstage/plugin-search-backend': pluginSearchBackend, + '@backstage/plugin-search-backend-module-pg': pluginSearchBackendModulePg, '@backstage/plugin-search-backend-node': pluginSearchBackendNode, '@backstage/plugin-tech-radar': pluginTechRadar, '@backstage/plugin-techdocs': pluginTechdocs, diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 618c06a0c3..3afed0ed82 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -116,10 +116,6 @@ catalog: target: https://github.com/backstage/software-templates/blob/main/scaffolder-templates/springboot-grpc-template/template.yaml rules: - allow: [Template] - - type: url - target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml - rules: - - allow: [Template] - type: url target: https://github.com/backstage/software-templates/blob/main/scaffolder-templates/docs-template/template.yaml rules: diff --git a/packages/create-app/templates/default-app/backstage.json.hbs b/packages/create-app/templates/default-app/backstage.json.hbs new file mode 100644 index 0000000000..2aa00ecaee --- /dev/null +++ b/packages/create-app/templates/default-app/backstage.json.hbs @@ -0,0 +1,3 @@ +{ + "version": "{{version 'root'}}" +} diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index e7c39add25..884eaf23cf 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -9,7 +9,7 @@ "dev": "concurrently \"yarn start\" \"yarn start-backend\"", "start": "yarn workspace app start", "start-backend": "yarn workspace backend start", - "build": "lerna run build", + "build": "backstage-cli repo build --all", "build-image": "yarn workspace backend build-image", "tsc": "tsc", "tsc:full": "tsc --skipLibCheck false --incremental false", @@ -17,8 +17,8 @@ "diff": "lerna run diff --", "test": "backstage-cli test", "test:all": "lerna run test -- --coverage", - "lint": "lerna run lint --since origin/master --", - "lint:all": "lerna run lint --", + "lint": "backstage-cli repo lint --since origin/master", + "lint:all": "backstage-cli repo lint", "prettier:check": "prettier --check .", "create-plugin": "backstage-cli create-plugin --scope internal", "remove-plugin": "backstage-cli remove-plugin" diff --git a/packages/create-app/templates/default-app/packages/app/.eslintrc.js b/packages/create-app/templates/default-app/packages/app/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/create-app/templates/default-app/packages/app/.eslintrc.js +++ b/packages/create-app/templates/default-app/packages/app/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs index 53e0590db4..71d2ac9b3b 100644 --- a/packages/create-app/templates/default-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs @@ -3,6 +3,9 @@ "version": "0.0.0", "private": true, "bundled": true, + "backstage": { + "role": "frontend" + }, "dependencies": { "@backstage/app-defaults": "^{{version '@backstage/app-defaults'}}", "@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}", @@ -49,13 +52,13 @@ "start-server-and-test": "^1.10.11" }, "scripts": { - "start": "backstage-cli app:serve", - "build": "backstage-cli app:build", - "clean": "backstage-cli clean", - "test": "backstage-cli test", + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "test": "backstage-cli package test", + "lint": "backstage-cli package lint", "test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev", "test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run", - "lint": "backstage-cli lint", "cy:dev": "cypress open", "cy:run": "cypress run" }, diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 6f00993273..f4ff424926 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -30,7 +30,7 @@ import { createApp } from '@backstage/app-defaults'; import { FlatRoutes } from '@backstage/core-app-api'; import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; import { PermissionedRoute } from '@backstage/plugin-permission-react'; -import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; const app = createApp({ apis, diff --git a/packages/create-app/templates/default-app/packages/backend/.eslintrc.js b/packages/create-app/templates/default-app/packages/backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/create-app/templates/default-app/packages/backend/.eslintrc.js +++ b/packages/create-app/templates/default-app/packages/backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 5dd70286bc..79d6772d7e 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -4,13 +4,16 @@ "main": "dist/index.cjs.js", "types": "src/index.ts", "private": true, + "backstage": { + "role": "backend" + }, "scripts": { - "build": "backstage-cli backend:bundle", + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", "build-image": "docker build ../.. -f Dockerfile --tag backstage", - "start": "backstage-cli backend:dev", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "clean": "backstage-cli clean", "migrate:create": "knex migrate:make -x ts" }, "dependencies": { @@ -28,6 +31,9 @@ "@backstage/plugin-proxy-backend": "^{{version '@backstage/plugin-proxy-backend'}}", "@backstage/plugin-scaffolder-backend": "^{{version '@backstage/plugin-scaffolder-backend'}}", "@backstage/plugin-search-backend": "^{{version '@backstage/plugin-search-backend'}}", + {{#if dbTypePG}} + "@backstage/plugin-search-backend-module-pg": "^{{version '@backstage/plugin-search-backend-module-pg'}}", + {{/if}} "@backstage/plugin-search-backend-node": "^{{version '@backstage/plugin-search-backend-node'}}", "@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}", "@gitbeaker/node": "^34.6.0", diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts index 6be2e9712d..a460fd8a6d 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -1,7 +1,5 @@ -import { DockerContainerRunner } from '@backstage/backend-common'; import { CatalogClient } from '@backstage/catalog-client'; import { createRouter } from '@backstage/plugin-scaffolder-backend'; -import Docker from 'dockerode'; import { Router } from 'express'; import type { PluginEnvironment } from '../types'; @@ -12,12 +10,8 @@ export default async function createPlugin({ reader, discovery, }: PluginEnvironment): Promise { - const dockerClient = new Docker(); - const containerRunner = new DockerContainerRunner({ dockerClient }); const catalogClient = new CatalogClient({ discoveryApi: discovery }); - return await createRouter({ - containerRunner, logger, config, database, diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs similarity index 72% rename from packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts rename to packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs index a0a1cc3701..d8ad991e8a 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs @@ -4,9 +4,12 @@ import { IndexBuilder, LunrSearchEngine, } from '@backstage/plugin-search-backend-node'; +{{#if dbTypePG}} +import { PgSearchEngine } from '@backstage/plugin-search-backend-module-pg'; +{{/if}} import { PluginEnvironment } from '../types'; -import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; +import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; export default async function createPlugin({ logger, @@ -16,14 +19,21 @@ export default async function createPlugin({ tokenManager, }: PluginEnvironment) { // Initialize a connection to a search engine. + {{#if dbTypeSqlite}} const searchEngine = new LunrSearchEngine({ logger }); + {{/if}} + {{#if dbTypePG}} + const searchEngine = (await PgSearchEngine.supported(database)) + ? await PgSearchEngine.from({ database }) + : new LunrSearchEngine({ logger }); + {{/if}} const indexBuilder = new IndexBuilder({ logger, searchEngine }); // Collators are responsible for gathering documents known to plugins. This // collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultCatalogCollator.fromConfig(config, { + factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery, tokenManager, }), @@ -32,7 +42,7 @@ export default async function createPlugin({ // collator gathers entities from techdocs. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultTechDocsCollator.fromConfig(config, { + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { discovery, logger, tokenManager, diff --git a/packages/dev-utils/.eslintrc.js b/packages/dev-utils/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/dev-utils/.eslintrc.js +++ b/packages/dev-utils/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 4d2354297e..32fbf7e9cb 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/dev-utils +## 0.2.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/app-defaults@0.2.1-next.0 + - @backstage/integration-react@0.1.25-next.0 + +## 0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-app-api@0.6.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/test-utils@0.3.0 + - @backstage/app-defaults@0.2.0 + - @backstage/integration-react@0.1.24 + ## 0.2.23 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 5c29c7dd0f..f9c3bbc166 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.2.23", + "version": "0.2.25-next.0", "private": false, "publishConfig": { "access": "public", @@ -33,14 +33,14 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/app-defaults": "^0.1.9", - "@backstage/core-app-api": "^0.5.4", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/catalog-model": "^0.11.0", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/test-utils": "^0.2.6", + "@backstage/app-defaults": "^0.2.1-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/integration-react": "^0.1.25-next.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", + "@backstage/test-utils": "^0.3.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -59,7 +59,7 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/e2e-test/.eslintrc.js b/packages/e2e-test/.eslintrc.js index 7c8a092081..f9fb2e27c9 100644 --- a/packages/e2e-test/.eslintrc.js +++ b/packages/e2e-test/.eslintrc.js @@ -1,5 +1,4 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { 'no-console': 0, 'import/no-extraneous-dependencies': [ @@ -12,4 +11,4 @@ module.exports = { }, ], }, -}; +}); diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index b17b0c14ce..4092b79fa0 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -37,7 +37,7 @@ "chalk": "^4.0.0", "commander": "^6.1.0", "cross-fetch": "^3.1.5", - "fs-extra": "9.1.0", + "fs-extra": "10.0.1", "handlebars": "^4.7.3", "pgtools": "^0.3.0", "puppeteer": "^13.1.1", diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index a7456f91dd..1ec9499b92 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -420,9 +420,15 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { if (isPostgres) { print('Dropping old DBs'); await Promise.all( - ['catalog', 'scaffolder', 'auth', 'identity', 'proxy', 'techdocs'].map( - name => dropDB(`backstage_plugin_${name}`), - ), + [ + 'catalog', + 'scaffolder', + 'auth', + 'identity', + 'proxy', + 'techdocs', + 'search', + ].map(name => dropDB(`backstage_plugin_${name}`)), ); print('Created DBs'); } diff --git a/packages/errors/.eslintrc.js b/packages/errors/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/errors/.eslintrc.js +++ b/packages/errors/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/errors/package.json b/packages/errors/package.json index b1c90f0371..de20e7b5f2 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -38,7 +38,7 @@ "serialize-error": "^8.0.1" }, "devDependencies": { - "@backstage/cli": "^0.14.0", + "@backstage/cli": "^0.15.2-next.0", "@types/jest": "^26.0.7" }, "files": [ diff --git a/packages/integration-react/.eslintrc.js b/packages/integration-react/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/integration-react/.eslintrc.js +++ b/packages/integration-react/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index 1b8f22bcfc..082b8ba701 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/integration-react +## 0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.1-next.0 + +## 0.1.24 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.23 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 27e4492e65..16863b2ef2 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-react", "description": "Frontend package for managing integrations towards external systems", - "version": "0.1.23", + "version": "0.1.25-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,9 +25,9 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/integration": "^0.7.5", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/integration": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -38,9 +38,9 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/integration/.eslintrc.js b/packages/integration/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/integration/.eslintrc.js +++ b/packages/integration/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index af764c3b69..faac399f63 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/integration +## 0.8.0 + +### Minor Changes + +- 34af86517c: ensure `apiBaseUrl` being set for Bitbucket integrations, replace hardcoded defaults + +### Patch Changes + +- 33d5e79822: Fix Bitbucket Cloud and Bitbucket Server line number reference. + ## 0.7.5 ### Patch Changes diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 10da05784c..9a6ba03f8e 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -88,7 +88,7 @@ export class BitbucketIntegration implements ScmIntegration { // @public export type BitbucketIntegrationConfig = { host: string; - apiBaseUrl?: string; + apiBaseUrl: string; token?: string; username?: string; appPassword?: string; diff --git a/packages/integration/package.json b/packages/integration/package.json index bf7c167bfa..880cb8c1cf 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration", "description": "Helpers for managing integrations towards external systems", - "version": "0.7.5", + "version": "0.8.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,9 +42,9 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/config-loader": "^0.9.6", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/config-loader": "^0.9.7-next.0", + "@backstage/test-utils": "^0.3.0", "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", "msw": "^0.35.0" diff --git a/packages/integration/src/bitbucket/config.ts b/packages/integration/src/bitbucket/config.ts index 1cd911aad3..44a2f0cc1f 100644 --- a/packages/integration/src/bitbucket/config.ts +++ b/packages/integration/src/bitbucket/config.ts @@ -36,12 +36,10 @@ export type BitbucketIntegrationConfig = { * The base URL of the API of this provider, e.g. "https://api.bitbucket.org/2.0", * with no trailing slash. * - * May be omitted specifically for Bitbucket Cloud; then it will be deduced. - * - * The API will always be preferred if both its base URL and a token are - * present. + * Values omitted at the optional property at the app-config will be deduced + * from the "host" value. */ - apiBaseUrl?: string; + apiBaseUrl: string; /** * The authorization token to use for requests to a Bitbucket Server provider. @@ -90,6 +88,8 @@ export function readBitbucketIntegrationConfig( apiBaseUrl = trimEnd(apiBaseUrl, '/'); } else if (host === BITBUCKET_HOST) { apiBaseUrl = BITBUCKET_API_BASE_URL; + } else { + apiBaseUrl = `https://${host}/rest/api/1.0`; } return { diff --git a/packages/integration/src/helpers.test.ts b/packages/integration/src/helpers.test.ts index a69c7faec9..60a2789f05 100644 --- a/packages/integration/src/helpers.test.ts +++ b/packages/integration/src/helpers.test.ts @@ -24,7 +24,10 @@ import { describe('basicIntegrations', () => { describe('byUrl', () => { it('handles hosts without a port', () => { - const integration = new BitbucketIntegration({ host: 'host.com' }); + const integration = new BitbucketIntegration({ + host: 'host.com', + apiBaseUrl: 'a', + }); const integrations = basicIntegrations( [integration], i => i.config.host, @@ -33,7 +36,10 @@ describe('basicIntegrations', () => { expect(integrations.byUrl('https://host.com:8080/a')).toBeUndefined(); }); it('handles hosts with a port', () => { - const integration = new BitbucketIntegration({ host: 'host.com:8080' }); + const integration = new BitbucketIntegration({ + host: 'host.com:8080', + apiBaseUrl: 'a', + }); const integrations = basicIntegrations( [integration], i => i.config.host, diff --git a/packages/release-manifests/.eslintrc.js b/packages/release-manifests/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/release-manifests/.eslintrc.js +++ b/packages/release-manifests/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/release-manifests/package.json b/packages/release-manifests/package.json index 7ec69a35e9..7959a17d0c 100644 --- a/packages/release-manifests/package.json +++ b/packages/release-manifests/package.json @@ -36,7 +36,7 @@ "cross-fetch": "^3.1.5" }, "devDependencies": { - "@backstage/test-utils": "^0.2.5", + "@backstage/test-utils": "^0.3.0", "msw": "^0.35.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" diff --git a/packages/search-common/.eslintrc.js b/packages/search-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/search-common/.eslintrc.js +++ b/packages/search-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/search-common/CHANGELOG.md b/packages/search-common/CHANGELOG.md index 71f2952d90..3b1b082083 100644 --- a/packages/search-common/CHANGELOG.md +++ b/packages/search-common/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/search-common +## 0.3.1-next.0 + +### Patch Changes + +- d52155466a: **DEPRECATION** + + The `@backstage/search-common` package is being renamed `@backstage/plugin-search-common`. We may continue to publish changes to `@backstage/search-common` for a time, but will stop doing so in the near future. If you depend on this package, you should update your dependencies to point at the renamed package. + +- Updated dependencies + - @backstage/plugin-search-common@0.3.1-next.0 + +## 0.3.0 + +### Minor Changes + +- 022507c860: **BREAKING** + + The Backstage Search Platform's indexing process has been rewritten as a stream + pipeline in order to improve efficiency and performance on large document sets. + + The concepts of `Collator` and `Decorator` have been replaced with readable and + transform object streams (respectively), as well as factory classes to + instantiate them. Accordingly, the `SearchEngine.index()` method has also been + replaced with a `getIndexer()` factory method that resolves to a writable + object stream. + + Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for further details. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.5.2 + ## 0.2.4 ### Patch Changes diff --git a/packages/search-common/README.md b/packages/search-common/README.md index 66828cc7af..3a75dac4c2 100644 --- a/packages/search-common/README.md +++ b/packages/search-common/README.md @@ -1,3 +1,7 @@ # @backstage/search-common +**WARNING**: This package is moving to `@backstage/plugin-search-common`. +Please update any dependencies you may have, as this package will no longer be +published or updated in the near future. + Common functionalities for Search, to be shared between various search-enabled plugins. diff --git a/packages/search-common/api-report.md b/packages/search-common/api-report.md index 65f9f25180..03965ace66 100644 --- a/packages/search-common/api-report.md +++ b/packages/search-common/api-report.md @@ -3,104 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { JsonObject } from '@backstage/types'; -import { Permission } from '@backstage/plugin-permission-common'; +export * from '@backstage/plugin-search-common'; -// Warning: (ae-missing-release-tag) "DocumentCollator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export interface DocumentCollator { - // (undocumented) - execute(): Promise; - readonly type: string; - readonly visibilityPermission?: Permission; -} - -// Warning: (ae-missing-release-tag) "DocumentDecorator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export interface DocumentDecorator { - // (undocumented) - execute(documents: IndexableDocument[]): Promise; - readonly types?: string[]; -} - -// Warning: (ae-missing-release-tag) "DocumentTypeInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export type DocumentTypeInfo = { - visibilityPermission?: Permission; -}; - -// Warning: (ae-missing-release-tag) "IndexableDocument" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export interface IndexableDocument { - authorization?: { - resourceRef: string; - }; - location: string; - text: string; - title: string; -} - -// Warning: (ae-missing-release-tag) "QueryRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type QueryRequestOptions = { - token?: string; -}; - -// Warning: (ae-missing-release-tag) "QueryTranslator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export type QueryTranslator = (query: SearchQuery) => unknown; - -// Warning: (ae-missing-release-tag) "SearchEngine" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export interface SearchEngine { - index(type: string, documents: IndexableDocument[]): Promise; - query( - query: SearchQuery, - options?: QueryRequestOptions, - ): Promise; - setTranslator(translator: QueryTranslator): void; -} - -// Warning: (ae-missing-release-tag) "SearchQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface SearchQuery { - // (undocumented) - filters?: JsonObject; - // (undocumented) - pageCursor?: string; - // (undocumented) - term: string; - // (undocumented) - types?: string[]; -} - -// Warning: (ae-missing-release-tag) "SearchResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface SearchResult { - // (undocumented) - document: IndexableDocument; - // (undocumented) - type: string; -} - -// Warning: (ae-missing-release-tag) "SearchResultSet" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface SearchResultSet { - // (undocumented) - nextPageCursor?: string; - // (undocumented) - previousPageCursor?: string; - // (undocumented) - results: SearchResult[]; -} +// (No @packageDocumentation comment for this package) ``` diff --git a/packages/search-common/package.json b/packages/search-common/package.json index ef56017405..d84bda9edc 100644 --- a/packages/search-common/package.json +++ b/packages/search-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/search-common", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", - "version": "0.2.4", + "version": "0.3.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -39,12 +39,9 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/types": "^0.1.3", - "@backstage/plugin-permission-common": "^0.5.1" - }, - "devDependencies": { - "@backstage/cli": "^0.14.0" + "@backstage/plugin-search-common": "^0.3.1-next.0" }, + "devDependencies": {}, "jest": { "roots": [ ".." diff --git a/packages/search-common/src/index.ts b/packages/search-common/src/index.ts index e71f8cd660..8e21c5f936 100644 --- a/packages/search-common/src/index.ts +++ b/packages/search-common/src/index.ts @@ -14,10 +14,4 @@ * limitations under the License. */ -/** - * Common functionalities for Search, to be shared between various search-enabled plugins - * - * @packageDocumentation - */ - -export * from './types'; +export * from '@backstage/plugin-search-common'; diff --git a/packages/techdocs-cli-embedded-app/.eslintrc.js b/packages/techdocs-cli-embedded-app/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/techdocs-cli-embedded-app/.eslintrc.js +++ b/packages/techdocs-cli-embedded-app/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 1302d1ca9d..51f0bc8b00 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,34 @@ # techdocs-cli-embedded-app +## 0.2.67-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.15.2-next.0 + - @backstage/plugin-catalog@0.10.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/plugin-techdocs@0.15.1-next.0 + - @backstage/app-defaults@0.2.1-next.0 + - @backstage/integration-react@0.1.25-next.0 + +## 0.2.66 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-techdocs@0.15.0 + - @backstage/plugin-catalog@0.9.1 + - @backstage/core-app-api@0.6.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/test-utils@0.3.0 + - @backstage/cli@0.15.0 + - @backstage/app-defaults@0.2.0 + - @backstage/integration-react@0.1.24 + ## 0.2.65 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 6719701378..700036dd3c 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,23 +1,23 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.65", + "version": "0.2.67-next.0", "private": true, "backstage": { "role": "frontend" }, "bundled": true, "dependencies": { - "@backstage/app-defaults": "^0.1.9", - "@backstage/catalog-model": "^0.11.0", - "@backstage/cli": "^0.14.1", + "@backstage/app-defaults": "^0.2.1-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/cli": "^0.15.2-next.0", "@backstage/config": "^0.1.15", - "@backstage/core-app-api": "^0.5.4", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/plugin-techdocs": "^0.14.0", - "@backstage/test-utils": "^0.2.6", + "@backstage/core-app-api": "^0.6.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/integration-react": "^0.1.25-next.0", + "@backstage/plugin-catalog": "^0.10.0-next.0", + "@backstage/plugin-techdocs": "^0.15.1-next.0", + "@backstage/test-utils": "^0.3.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -29,7 +29,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/techdocs-cli-embedded-app/src/apis.ts b/packages/techdocs-cli-embedded-app/src/apis.ts index 6234e3b719..1fe6ba8086 100644 --- a/packages/techdocs-cli-embedded-app/src/apis.ts +++ b/packages/techdocs-cli-embedded-app/src/apis.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { scmIntegrationsApiRef, @@ -81,7 +81,7 @@ class TechDocsDevStorageApi implements TechDocsStorageApi { return this.configApi.getString('techdocs.builder'); } - async getEntityDocs(_entityId: EntityName, path: string) { + async getEntityDocs(_entityId: CompoundEntityRef, path: string) { const apiOrigin = await this.getApiOrigin(); // Irrespective of the entity, use mkdocs server to find the file for the path. const url = `${apiOrigin}/${path}`; @@ -97,7 +97,7 @@ class TechDocsDevStorageApi implements TechDocsStorageApi { return request.text(); } - async syncEntityDocs(_: EntityName): Promise { + async syncEntityDocs(_: CompoundEntityRef): Promise { // this is just stub of this function as we don't need to check if docs are up to date, // we always want to retrigger a new build return 'cached'; @@ -106,7 +106,7 @@ class TechDocsDevStorageApi implements TechDocsStorageApi { // Used by transformer to modify the request to assets (CSS, Image) from inside the HTML. async getBaseUrl( oldBaseUrl: string, - _entityId: EntityName, + _entityId: CompoundEntityRef, path: string, ): Promise { const apiOrigin = await this.getApiOrigin(); @@ -154,7 +154,7 @@ class TechDocsDevApi implements TechDocsApi { }; } - async getTechDocsMetadata(_entityId: EntityName) { + async getTechDocsMetadata(_entityId: CompoundEntityRef) { return { site_name: 'Live preview environment', site_description: '', diff --git a/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx index d9a518c165..f2dcefc00e 100644 --- a/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx +++ b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx @@ -29,7 +29,7 @@ import LightIcon from '@material-ui/icons/Brightness7'; import DarkIcon from '@material-ui/icons/Brightness4'; import { lightTheme, darkTheme } from '@backstage/theme'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Content } from '@backstage/core-components'; @@ -127,7 +127,7 @@ const TechDocsPageContent = ({ onReady, entityRef, }: { - entityRef: EntityName; + entityRef: CompoundEntityRef; onReady: () => void; }) => { const classes = useStyles(); diff --git a/packages/techdocs-cli/.eslintrc.js b/packages/techdocs-cli/.eslintrc.js index 884f559c0f..e2a53a6ad2 100644 --- a/packages/techdocs-cli/.eslintrc.js +++ b/packages/techdocs-cli/.eslintrc.js @@ -1,11 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], - overrides: [ - { - files: ['**/*.ts?(x)'], - rules: { - 'no-restricted-imports': 0, - }, - }, - ], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index bc23499662..e668940718 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,26 @@ # @techdocs/cli +## 0.8.17-next.0 + +### Patch Changes + +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- 91bf1e6c1a: Use `@backstage/plugin-techdocs-node` package instead of `@backstage/techdocs-common`. +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-techdocs-node@0.11.12-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.8.16 + +### Patch Changes + +- 853efd42bd: Bump `@backstage/techdocs-common` to `0.11.10` to use `spotify/techdocs:v0.3.7` which upgrades `mkdocs-theme` as a dependency of `mkdocs-techdocs-core`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/backend-common@0.12.0 + - @backstage/techdocs-common@0.11.11 + ## 0.8.15 ### Patch Changes diff --git a/packages/techdocs-cli/e2e-test.config.js b/packages/techdocs-cli/e2e-test.config.js new file mode 100644 index 0000000000..2fa92462ab --- /dev/null +++ b/packages/techdocs-cli/e2e-test.config.js @@ -0,0 +1,22 @@ +/* + * Copyright 2022 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. + */ + +const path = require('path'); + +module.exports = require('@backstage/cli/config/jest').then(baseConfig => ({ + ...baseConfig, + rootDir: path.resolve(__dirname, 'e2e-tests'), +})); diff --git a/packages/techdocs-cli/src/e2e.test.ts b/packages/techdocs-cli/e2e-tests/techdocs-cli.test.ts similarity index 98% rename from packages/techdocs-cli/src/e2e.test.ts rename to packages/techdocs-cli/e2e-tests/techdocs-cli.test.ts index 7cdd867d0f..d9d736cb6d 100644 --- a/packages/techdocs-cli/src/e2e.test.ts +++ b/packages/techdocs-cli/e2e-tests/techdocs-cli.test.ts @@ -58,7 +58,7 @@ const timeout = 25000; jest.setTimeout(timeout * 2); describe('end-to-end', () => { - const cwd = path.resolve(__dirname, 'example-docs'); + const cwd = path.resolve(__dirname, '../src/example-docs'); afterEach(async () => { // On Windows the pid of a spawned process may be wrong diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 929564e01c..758cab9856 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "0.8.15", + "version": "0.8.17-next.0", "private": false, "publishConfig": { "access": "public" @@ -27,9 +27,9 @@ "build": "backstage-cli package build", "clean": "backstage-cli package clean", "lint": "backstage-cli package lint", - "test": "backstage-cli package test --testPathIgnorePatterns=src/e2e.test.ts", - "test:e2e": "backstage-cli test src/e2e.test.ts", - "test:e2e:ci": "backstage-cli test --watchAll=false --ci src/e2e.test.ts", + "test": "backstage-cli package test", + "test:e2e": "backstage-cli test --config e2e-test.config.js", + "test:e2e:ci": "backstage-cli test --config e2e-test.config.js --watchAll=false --ci", "test:cypress": "cypress open", "prepack": "./scripts/prepack.sh" }, @@ -37,7 +37,7 @@ "techdocs-cli": "bin/techdocs-cli" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/commander": "^2.12.2", "@types/fs-extra": "^9.0.6", "@types/http-proxy": "^1.17.4", @@ -62,15 +62,15 @@ "ext": "ts" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/cli-common": "^0.1.8", "@backstage/config": "^0.1.15", - "@backstage/techdocs-common": "^0.11.10", + "@backstage/plugin-techdocs-node": "^0.11.12-next.0", "@types/dockerode": "^3.3.0", "commander": "^6.1.0", "dockerode": "^3.3.1", - "fs-extra": "^9.0.1", + "fs-extra": "^10.0.1", "http-proxy": "^1.18.1", "react-dev-utils": "^12.0.0-next.60", "serve-handler": "^6.1.3", diff --git a/packages/techdocs-cli/src/commands/generate/generate.ts b/packages/techdocs-cli/src/commands/generate/generate.ts index a255805139..98529698ca 100644 --- a/packages/techdocs-cli/src/commands/generate/generate.ts +++ b/packages/techdocs-cli/src/commands/generate/generate.ts @@ -21,7 +21,7 @@ import Docker from 'dockerode'; import { TechdocsGenerator, ParsedLocationAnnotation, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import { DockerContainerRunner } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { @@ -31,7 +31,7 @@ import { import { stdout } from 'process'; export default async function generate(cmd: Command) { - // Use techdocs-common package to generate docs. Keep consistency between Backstage and CI generating docs. + // Use techdocs-node package to generate docs. Keep consistency between Backstage and CI generating docs. // Docs can be prepared using actions/checkout or git clone, or similar paradigms on CI. The TechDocs CI workflow // will run on the CI pipeline containing the documentation files. @@ -39,6 +39,7 @@ export default async function generate(cmd: Command) { const sourceDir = resolve(cmd.sourceDir); const outputDir = resolve(cmd.outputDir); + const omitTechdocsCorePlugin = cmd.omitTechdocsCoreMkdocsPlugin; const dockerImage = cmd.dockerImage; const pullImage = cmd.pull; @@ -55,6 +56,9 @@ export default async function generate(cmd: Command) { runIn: cmd.docker ? 'docker' : 'local', dockerImage, pullImage, + mkdocs: { + omitTechdocsCorePlugin, + }, }, }, }); @@ -74,7 +78,7 @@ export default async function generate(cmd: Command) { } } - // Generate docs using @backstage/techdocs-common + // Generate docs using @backstage/plugin-techdocs-node const techdocsGenerator = await TechdocsGenerator.fromConfig(config, { logger, containerRunner, diff --git a/packages/techdocs-cli/src/commands/index.ts b/packages/techdocs-cli/src/commands/index.ts index 115fe8e364..6ad62c5668 100644 --- a/packages/techdocs-cli/src/commands/index.ts +++ b/packages/techdocs-cli/src/commands/index.ts @@ -15,7 +15,7 @@ */ import { CommanderStatic } from 'commander'; -import { TechdocsGenerator } from '@backstage/techdocs-common'; +import { TechdocsGenerator } from '@backstage/plugin-techdocs-node'; const defaultDockerImage = TechdocsGenerator.defaultDockerImage; @@ -54,6 +54,11 @@ export function registerCommands(program: CommanderStatic) { 'A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json.', ) .option('-v --verbose', 'Enable verbose output.', false) + .option( + '--omitTechdocsCoreMkdocsPlugin', + "Don't patch MkDocs file automatically with techdocs-core plugin.", + false, + ) .alias('build') .action(lazy(() => import('./generate/generate').then(m => m.default))); diff --git a/packages/techdocs-cli/src/commands/migrate/migrate.ts b/packages/techdocs-cli/src/commands/migrate/migrate.ts index 1cd8fca65b..067bf1f173 100644 --- a/packages/techdocs-cli/src/commands/migrate/migrate.ts +++ b/packages/techdocs-cli/src/commands/migrate/migrate.ts @@ -15,7 +15,7 @@ */ import { SingleHostDiscovery } from '@backstage/backend-common'; -import { Publisher } from '@backstage/techdocs-common'; +import { Publisher } from '@backstage/plugin-techdocs-node'; import { Command } from 'commander'; import { createLogger } from '../../lib/utility'; import { PublisherConfig } from '../../lib/PublisherConfig'; diff --git a/packages/techdocs-cli/src/commands/publish/publish.ts b/packages/techdocs-cli/src/commands/publish/publish.ts index 11a47d4a71..c84180b032 100644 --- a/packages/techdocs-cli/src/commands/publish/publish.ts +++ b/packages/techdocs-cli/src/commands/publish/publish.ts @@ -18,7 +18,7 @@ import { resolve } from 'path'; import { Command } from 'commander'; import { createLogger } from '../../lib/utility'; import { SingleHostDiscovery } from '@backstage/backend-common'; -import { Publisher } from '@backstage/techdocs-common'; +import { Publisher } from '@backstage/plugin-techdocs-node'; import { Entity } from '@backstage/catalog-model'; import { PublisherConfig } from '../../lib/PublisherConfig'; diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index a6a671c2df..d5b8191cf9 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -37,6 +37,7 @@ function findPreviewBundlePath(): string { // This can be tested by running `yarn pack` and extracting the resulting tarball into a directory. // Within the extracted directory, run `npm install --only=prod`. // Once that's done you can test the CLI in any directory using `node /package `. + // eslint-disable-next-line no-restricted-syntax return findPaths(__dirname).resolveOwn('dist/embedded-app'); } } diff --git a/packages/techdocs-cli/src/lib/utility.ts b/packages/techdocs-cli/src/lib/utility.ts index 87dbbfd08d..3f91f3c82d 100644 --- a/packages/techdocs-cli/src/lib/utility.ts +++ b/packages/techdocs-cli/src/lib/utility.ts @@ -16,7 +16,7 @@ import { RemoteProtocol, ParsedLocationAnnotation, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import * as winston from 'winston'; export const convertTechDocsRefToLocationAnnotation = ( diff --git a/packages/techdocs-common/.eslintrc.js b/packages/techdocs-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/techdocs-common/.eslintrc.js +++ b/packages/techdocs-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index c99295e8db..87c6ea0da9 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/techdocs-common +## 0.11.12-next.0 + +### Patch Changes + +- cea6f10b97: **DEPRECATION** + + The `@backstage/techdocs-common` package is being renamed `@backstage/plugin-techdocs-node`. We may continue to publish changes to `@backstage/techdocs-common` for a time, but will stop doing so in the near future. If you depend on this package, you should update your dependencies to point at the renamed package. + +- Updated dependencies + - @backstage/plugin-techdocs-node@0.11.12-next.0 + +## 0.11.11 + +### Patch Changes + +- 955be6bc7d: adds passing projectID to the Storage client +- ff0a16fb1a: Modify techdocs builder to automatically append techdocs-core plugin to mkdocs.yaml file if it is missing. Adds an optional configuration item if this plugin needs to be omitted. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/backend-common@0.12.0 + - @backstage/integration@0.8.0 + - @backstage/search-common@0.3.0 + ## 0.11.10 ### Patch Changes diff --git a/packages/techdocs-common/README.md b/packages/techdocs-common/README.md index bbaac85e9b..452b09b60b 100644 --- a/packages/techdocs-common/README.md +++ b/packages/techdocs-common/README.md @@ -1,15 +1,19 @@ # @backstage/techdocs-common -Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli +**WARNING**: This package is moving to `@backstage/plugin-techdocs-node`. +Please update any dependencies you may have, as this package will no longer be +published or updated in the near future. + +Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli This package is used by `techdocs-backend` to serve docs from different types of publishers (Google GCS, Local, etc.). It is also used to build docs and publish them to storage, by both `techdocs-backend` and `techdocs-cli`. ## Usage -Create a preparer instance from the [preparers available](/packages/techdocs-common/src/stages/prepare) at which takes an Entity instance. -Run the [docs generator](/packages/techdocs-common/src/stages/generate) on the prepared directory. -Publish the generated directory files to a [storage](/packages/techdocs-common/src/stages/publish) of your choice. +Create a preparer instance from the [preparers available](/plugins/techdocs-node/src/stages/prepare) at which takes an Entity instance. +Run the [docs generator](/plugins/techdocs-node/src/stages/generate) on the prepared directory. +Publish the generated directory files to a [storage](/plugins/techdocs-node/src/stages/publish) of your choice. Example: diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md index 7c4fc95abb..62851bbd68 100644 --- a/packages/techdocs-common/api-report.md +++ b/packages/techdocs-common/api-report.md @@ -3,254 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// +export * from '@backstage/plugin-techdocs-node'; -import { Config } from '@backstage/config'; -import { ContainerRunner } from '@backstage/backend-common'; -import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; -import express from 'express'; -import { IndexableDocument } from '@backstage/search-common'; -import { Logger as Logger_2 } from 'winston'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { ScmIntegrationRegistry } from '@backstage/integration'; -import { UrlReader } from '@backstage/backend-common'; -import { Writable } from 'stream'; - -// @public -export class DirectoryPreparer implements PreparerBase { - // @deprecated - constructor(config: Config, _logger: Logger_2 | null, reader: UrlReader); - static fromConfig( - config: Config, - { logger, reader }: PreparerConfig, - ): DirectoryPreparer; - prepare(entity: Entity, options?: PreparerOptions): Promise; -} - -// @public -export type ETag = string; - -// @public -export type GeneratorBase = { - run(opts: GeneratorRunOptions): Promise; -}; - -// @public -export type GeneratorBuilder = { - register(protocol: SupportedGeneratorKey, generator: GeneratorBase): void; - get(entity: Entity): GeneratorBase; -}; - -// @public -export type GeneratorOptions = { - containerRunner: ContainerRunner; - logger: Logger_2; -}; - -// @public -export type GeneratorRunOptions = { - inputDir: string; - outputDir: string; - parsedLocationAnnotation?: ParsedLocationAnnotation; - etag?: string; - logger: Logger_2; - logStream?: Writable; -}; - -// @public -export class Generators implements GeneratorBuilder { - static fromConfig( - config: Config, - options: { - logger: Logger_2; - containerRunner: ContainerRunner; - }, - ): Promise; - get(entity: Entity): GeneratorBase; - register(generatorKey: SupportedGeneratorKey, generator: GeneratorBase): void; -} - -// @public -export const getDocFilesFromRepository: ( - reader: UrlReader, - entity: Entity, - opts?: - | { - etag?: string | undefined; - logger?: Logger_2 | undefined; - } - | undefined, -) => Promise; - -// @public -export const getLocationForEntity: ( - entity: Entity, - scmIntegration: ScmIntegrationRegistry, -) => ParsedLocationAnnotation; - -// @public -export type MigrateRequest = { - removeOriginal?: boolean; - concurrency?: number; -}; - -// @public -export type ParsedLocationAnnotation = { - type: RemoteProtocol; - target: string; -}; - -// @public -export const parseReferenceAnnotation: ( - annotationName: string, - entity: Entity, -) => ParsedLocationAnnotation; - -// @public -export type PreparerBase = { - prepare(entity: Entity, options?: PreparerOptions): Promise; -}; - -// @public -export type PreparerBuilder = { - register(protocol: RemoteProtocol, preparer: PreparerBase): void; - get(entity: Entity): PreparerBase; -}; - -// @public -export type PreparerConfig = { - logger: Logger_2; - reader: UrlReader; -}; - -// @public -export type PreparerOptions = { - logger?: Logger_2; - etag?: ETag; -}; - -// @public -export type PreparerResponse = { - preparedDir: string; - etag: ETag; -}; - -// @public -export class Preparers implements PreparerBuilder { - static fromConfig( - backstageConfig: Config, - { logger, reader }: PreparerConfig, - ): Promise; - get(entity: Entity): PreparerBase; - register(protocol: RemoteProtocol, preparer: PreparerBase): void; -} - -// @public -export class Publisher { - static fromConfig( - config: Config, - { logger, discovery }: PublisherFactory, - ): Promise; -} - -// @public -export interface PublisherBase { - docsRouter(): express.Handler; - fetchTechDocsMetadata(entityName: EntityName): Promise; - getReadiness(): Promise; - hasDocsBeenGenerated(entityName: Entity): Promise; - migrateDocsCase?(migrateRequest: MigrateRequest): Promise; - publish(request: PublishRequest): Promise; -} - -// @public -export type PublisherFactory = { - logger: Logger_2; - discovery: PluginEndpointDiscovery; -}; - -// @public -export type PublisherType = - | 'local' - | 'googleGcs' - | 'awsS3' - | 'azureBlobStorage' - | 'openStackSwift'; - -// @public -export type PublishRequest = { - entity: Entity; - directory: string; -}; - -// @public -export type PublishResponse = { - remoteUrl?: string; - objects?: string[]; -} | void; - -// @public -export type ReadinessResponse = { - isAvailable: boolean; -}; - -// @public -export type RemoteProtocol = 'url' | 'dir'; - -// @public -export type SupportedGeneratorKey = 'techdocs' | string; - -// @public -export interface TechDocsDocument extends IndexableDocument { - kind: string; - lifecycle: string; - name: string; - namespace: string; - owner: string; - path: string; -} - -// @public -export class TechdocsGenerator implements GeneratorBase { - constructor(options: { - logger: Logger_2; - containerRunner: ContainerRunner; - config: Config; - scmIntegrations: ScmIntegrationRegistry; - }); - static readonly defaultDockerImage = 'spotify/techdocs:v0.3.7'; - static fromConfig( - config: Config, - options: GeneratorOptions, - ): TechdocsGenerator; - run(options: GeneratorRunOptions): Promise; -} - -// @public -export type TechDocsMetadata = { - site_name: string; - site_description: string; - etag: string; - build_timestamp: number; - files?: string[]; -}; - -// @public -export const transformDirLocation: ( - entity: Entity, - dirAnnotation: ParsedLocationAnnotation, - scmIntegrations: ScmIntegrationRegistry, -) => { - type: 'dir' | 'url'; - target: string; -}; - -// @public -export class UrlPreparer implements PreparerBase { - // @deprecated - constructor(reader: UrlReader, logger: Logger_2); - static fromConfig({ reader, logger }: PreparerConfig): UrlPreparer; - prepare(entity: Entity, options?: PreparerOptions): Promise; -} +// (No @packageDocumentation comment for this package) ``` diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 731e967637..6d897a2f08 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", - "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.11.10", + "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", + "version": "0.11.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -40,39 +40,9 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@azure/identity": "^2.0.1", - "@azure/storage-blob": "^12.5.0", - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-model": "^0.11.0", - "@backstage/config": "^0.1.15", - "@backstage/errors": "^0.2.2", - "@backstage/search-common": "^0.2.4", - "@backstage/integration": "^0.7.5", - "@google-cloud/storage": "^5.6.0", - "@trendyol-js/openstack-swift-sdk": "^0.0.5", - "@types/express": "^4.17.6", - "aws-sdk": "^2.840.0", - "express": "^4.17.1", - "fs-extra": "9.1.0", - "git-url-parse": "^11.6.0", - "js-yaml": "^4.0.0", - "json5": "^2.1.3", - "mime-types": "^2.1.27", - "mock-fs": "^5.1.0", - "p-limit": "^3.1.0", - "recursive-readdir": "^2.2.2", - "winston": "^3.2.1" - }, - "devDependencies": { - "@backstage/cli": "^0.14.1", - "@types/fs-extra": "^9.0.5", - "@types/js-yaml": "^4.0.0", - "@types/mime-types": "^2.1.0", - "@types/mock-fs": "^4.13.0", - "@types/recursive-readdir": "^2.2.0", - "@types/supertest": "^2.0.8", - "supertest": "^6.1.3" + "@backstage/plugin-techdocs-node": "^0.11.12-next.0" }, + "devDependencies": {}, "jest": { "roots": [ ".." diff --git a/packages/techdocs-common/src/index.ts b/packages/techdocs-common/src/index.ts index a2acf84611..a881eef5ea 100644 --- a/packages/techdocs-common/src/index.ts +++ b/packages/techdocs-common/src/index.ts @@ -14,12 +14,4 @@ * limitations under the License. */ -/** - * Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli - * - * @packageDocumentation - */ - -export * from './stages'; -export * from './helpers'; -export * from './techdocsTypes'; +export * from '@backstage/plugin-techdocs-node'; diff --git a/packages/test-utils/.eslintrc.js b/packages/test-utils/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/test-utils/.eslintrc.js +++ b/packages/test-utils/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 4de90a4aa7..2f9deae04f 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/test-utils +## 0.3.0 + +### Minor Changes + +- bb2bb36651: **BREAKING**: Removed the deprecated `get` method from `StorageAPI` and its implementations, this method has been replaced by the `snapshot` method. The return value from snapshot no longer includes `newValue` which has been replaced by `value`. For getting notified when a value changes, use `observe# @backstage/test-utils. +- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@0.6.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-permission-react@0.3.3 + ## 0.2.6 ### Patch Changes diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index 4e5361ff80..2f2671885e 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -164,8 +164,6 @@ export class MockStorageApi implements StorageApi { // (undocumented) forBucket(name: string): StorageApi; // (undocumented) - get(key: string): T | undefined; - // (undocumented) observe$(key: string): Observable>; // (undocumented) remove(key: string): Promise; diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 5465f7783a..2f0aa0e988 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.2.6", + "version": "0.3.0", "private": false, "publishConfig": { "access": "public", @@ -34,10 +34,10 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/core-app-api": "^0.5.4", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-permission-react": "^0.3.2", + "@backstage/core-app-api": "^0.6.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-permission-react": "^0.3.3", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", @@ -55,7 +55,7 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "msw": "^0.35.0" diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts index c974198be1..63aa4235e1 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts @@ -24,7 +24,7 @@ describe('WebStorage Storage API', () => { it('should return undefined for values which are unset', async () => { const storage = createMockStorage(); - expect(storage.get('myfakekey')).toBeUndefined(); + expect(storage.snapshot('myfakekey').value).toBeUndefined(); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'absent', @@ -33,32 +33,29 @@ describe('WebStorage Storage API', () => { }); }); - it('should allow the setting and getting of the simple data structures', async () => { + it('should allow the setting and snapshotting of the simple data structures', async () => { const storage = createMockStorage(); await storage.set('myfakekey', 'helloimastring'); await storage.set('mysecondfakekey', 1234); await storage.set('mythirdfakekey', true); - expect(storage.get('myfakekey')).toBe('helloimastring'); - expect(storage.get('mysecondfakekey')).toBe(1234); - expect(storage.get('mythirdfakekey')).toBe(true); + expect(storage.snapshot('myfakekey').value).toBe('helloimastring'); + expect(storage.snapshot('mysecondfakekey').value).toBe(1234); + expect(storage.snapshot('mythirdfakekey').value).toBe(true); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'present', value: 'helloimastring', - newValue: 'helloimastring', }); expect(storage.snapshot('mysecondfakekey')).toEqual({ key: 'mysecondfakekey', presence: 'present', value: 1234, - newValue: 1234, }); expect(storage.snapshot('mythirdfakekey')).toEqual({ key: 'mythirdfakekey', presence: 'present', value: true, - newValue: true, }); }); @@ -72,12 +69,11 @@ describe('WebStorage Storage API', () => { await storage.set('myfakekey', mockData); - expect(storage.get('myfakekey')).toEqual(mockData); + expect(storage.snapshot('myfakekey').value).toEqual(mockData); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'present', value: mockData, - newValue: mockData, }); }); @@ -107,7 +103,6 @@ describe('WebStorage Storage API', () => { key: 'correctKey', presence: 'present', value: mockData, - newValue: mockData, }); }); @@ -153,9 +148,11 @@ describe('WebStorage Storage API', () => { await firstStorage.set(keyName, 'boop'); await secondStorage.set(keyName, 'deerp'); - expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName)); - expect(firstStorage.get(keyName)).toBe('boop'); - expect(secondStorage.get(keyName)).toBe('deerp'); + expect(firstStorage.snapshot(keyName)).not.toBe( + secondStorage.snapshot(keyName), + ); + expect(firstStorage.snapshot(keyName).value).toBe('boop'); + expect(secondStorage.snapshot(keyName).value).toBe('deerp'); expect(firstStorage.snapshot(keyName)).not.toEqual( secondStorage.snapshot(keyName), ); @@ -163,13 +160,11 @@ describe('WebStorage Storage API', () => { key: keyName, presence: 'present', value: 'boop', - newValue: 'boop', }); expect(secondStorage.snapshot(keyName)).toEqual({ key: keyName, presence: 'present', value: 'deerp', - newValue: 'deerp', }); }); @@ -186,7 +181,7 @@ describe('WebStorage Storage API', () => { await firstStorage.set('test2', { error: true }); - expect(secondStorage.get('deep/test2')).toBe(undefined); + expect(secondStorage.snapshot('deep/test2').value).toBe(undefined); expect(secondStorage.snapshot('deep/test2')).toMatchObject({ presence: 'absent', }); @@ -201,19 +196,17 @@ describe('WebStorage Storage API', () => { await firstStorage.set('test2', true); - expect(firstStorage.get('test2')).toBe(true); - expect(secondStorage.get('test2')).toBe(undefined); + expect(firstStorage.snapshot('test2').value).toBe(true); + expect(secondStorage.snapshot('test2').value).toBe(undefined); expect(firstStorage.snapshot('test2')).toEqual({ key: 'test2', presence: 'present', value: true, - newValue: true, }); expect(secondStorage.snapshot('test2')).toEqual({ key: 'test2', presence: 'absent', value: undefined, - newValue: undefined, }); }); diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts index 25024f2211..6ce91a4a31 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts @@ -61,10 +61,6 @@ export class MockStorageApi implements StorageApi { return this.bucketStorageApis.get(name)!; } - get(key: string): T | undefined { - return this.snapshot(key).value as T | undefined; - } - snapshot(key: string): StorageValueSnapshot { if (this.data.hasOwnProperty(this.getKeyName(key))) { const data = this.data[this.getKeyName(key)]; @@ -72,14 +68,12 @@ export class MockStorageApi implements StorageApi { key, presence: 'present', value: data, - newValue: data, }; } return { key, presence: 'absent', value: undefined, - newValue: undefined, }; } @@ -95,7 +89,6 @@ export class MockStorageApi implements StorageApi { key, presence: 'present', value: serialized, - newValue: serialized, }); } @@ -105,7 +98,6 @@ export class MockStorageApi implements StorageApi { key, presence: 'absent', value: undefined, - newValue: undefined, }); } diff --git a/packages/test-utils/src/testUtils/defaultApis.ts b/packages/test-utils/src/testUtils/defaultApis.ts index f06e1ba6b7..1ff10bfe6f 100644 --- a/packages/test-utils/src/testUtils/defaultApis.ts +++ b/packages/test-utils/src/testUtils/defaultApis.ts @@ -21,16 +21,13 @@ import { ErrorAlerter, GoogleAuth, GithubAuth, - OAuth2, OktaAuth, GitlabAuth, - Auth0Auth, MicrosoftAuth, BitbucketAuth, OAuthRequestManager, WebStorage, UrlPatternDiscovery, - SamlAuth, OneLoginAuth, UnhandledErrorForwarder, AtlassianAuth, @@ -45,16 +42,12 @@ import { oauthRequestApiRef, googleAuthApiRef, githubAuthApiRef, - oauth2ApiRef, oktaAuthApiRef, gitlabAuthApiRef, - auth0AuthApiRef, microsoftAuthApiRef, storageApiRef, configApiRef, - samlAuthApiRef, oneloginAuthApiRef, - oidcAuthApiRef, bitbucketAuthApiRef, atlassianAuthApiRef, } from '@backstage/core-plugin-api'; @@ -158,46 +151,6 @@ export const defaultApis = [ environment: configApi.getOptionalString('auth.environment'), }), }), - createApiFactory({ - api: auth0AuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - Auth0Auth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oauth2ApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: samlAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, configApi }) => - SamlAuth.create({ - discoveryApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), createApiFactory({ api: oneloginAuthApiRef, deps: { @@ -212,25 +165,6 @@ export const defaultApis = [ environment: configApi.getOptionalString('auth.environment'), }), }), - createApiFactory({ - api: oidcAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider: { - id: 'oidc', - title: 'Your Identity Provider', - icon: () => null, - }, - environment: configApi.getOptionalString('auth.environment'), - }), - }), createApiFactory({ api: bitbucketAuthApiRef, deps: { diff --git a/packages/test-utils/src/testUtils/logCollector.ts b/packages/test-utils/src/testUtils/logCollector.ts index b9fa6220ed..7529a00e7a 100644 --- a/packages/test-utils/src/testUtils/logCollector.ts +++ b/packages/test-utils/src/testUtils/logCollector.ts @@ -18,37 +18,44 @@ /** * Severity levels of {@link CollectedLogs} - * @public */ + * @public + */ export type LogFuncs = 'log' | 'warn' | 'error'; /** * AsyncLogCollector type used in {@link (withLogCollector:1)} callback function. - * @public */ + * @public + */ export type AsyncLogCollector = () => Promise; /** * SyncLogCollector type used in {@link (withLogCollector:2)} callback function. - * @public */ + * @public + */ export type SyncLogCollector = () => void; /** * Union type used in {@link (withLogCollector:3)} callback function. - * @public */ + * @public + */ export type LogCollector = AsyncLogCollector | SyncLogCollector; /** * Map of severity level and corresponding log lines. - * @public */ + * @public + */ export type CollectedLogs = { [key in T]: string[] }; const allCategories = ['log', 'warn', 'error']; /** * Asynchronous log collector with that collects all categories - * @public */ + * @public + */ export function withLogCollector( callback: AsyncLogCollector, ): Promise>; /** * Synchronous log collector with that collects all categories - * @public */ + * @public + */ export function withLogCollector( callback: SyncLogCollector, ): CollectedLogs; @@ -64,7 +71,8 @@ export function withLogCollector( /** * Synchronous log collector with that only collects selected categories - * @public */ + * @public + */ export function withLogCollector( logsToCollect: T[], callback: SyncLogCollector, @@ -73,7 +81,7 @@ export function withLogCollector( /** * Log collector that collect logs either from a sync or async collector. * @public - * */ + */ export function withLogCollector( logsToCollect: LogFuncs[] | LogCollector, callback?: LogCollector, diff --git a/packages/theme/.eslintrc.js b/packages/theme/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/theme/.eslintrc.js +++ b/packages/theme/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/theme/package.json b/packages/theme/package.json index 17404e8db0..cab489444a 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -36,7 +36,7 @@ "@material-ui/core": "^4.12.2" }, "devDependencies": { - "@backstage/cli": "^0.14.0" + "@backstage/cli": "^0.15.2-next.0" }, "files": [ "dist" diff --git a/packages/types/.eslintrc.js b/packages/types/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/types/.eslintrc.js +++ b/packages/types/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/types/package.json b/packages/types/package.json index ab0740d568..e71e32a6c2 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -34,7 +34,7 @@ }, "dependencies": {}, "devDependencies": { - "@backstage/cli": "^0.14.0", + "@backstage/cli": "^0.15.2-next.0", "@types/zen-observable": "^0.8.0", "zen-observable": "^0.8.15" }, diff --git a/packages/version-bridge/.eslintrc.js b/packages/version-bridge/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/version-bridge/.eslintrc.js +++ b/packages/version-bridge/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json index 485b0417cf..463e7841ed 100644 --- a/packages/version-bridge/package.json +++ b/packages/version-bridge/package.json @@ -37,7 +37,7 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.0", + "@backstage/cli": "^0.15.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2" diff --git a/plugins/airbrake-backend/.eslintrc.js b/plugins/airbrake-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/airbrake-backend/.eslintrc.js +++ b/plugins/airbrake-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/airbrake-backend/CHANGELOG.md b/plugins/airbrake-backend/CHANGELOG.md index 148dbdeb07..9826c90e72 100644 --- a/plugins/airbrake-backend/CHANGELOG.md +++ b/plugins/airbrake-backend/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-airbrake-backend +## 0.2.2-next.0 + +### Patch Changes + +- 4f79204c47: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + +## 0.2.1 + +### Patch Changes + +- 3c1d3cb07e: The Airbrake plugin installation instructions have been updated to work better and conform to how the frontend and backend plugins are supposed to be integrated into a Backstage instance. +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.2.0 ### Minor Changes diff --git a/plugins/airbrake-backend/README.md b/plugins/airbrake-backend/README.md index 9c9b56ade4..21056229de 100644 --- a/plugins/airbrake-backend/README.md +++ b/plugins/airbrake-backend/README.md @@ -24,7 +24,7 @@ This method of serving the plugin provides quicker iteration speed and a faster 3. Go into the plugin's directory and run it in standalone mode by running `yarn start`. -Access it from http://localhost:7007/api/airbrake. Or use the Airbrake plugin which will talk to it automatically. +Access it from http://localhost:7007/api/airbrake. Or use the [Airbrake plugin in standalone mode](../airbrake/README.md#local-development) which will talk to it automatically. Here are some example endpoints: diff --git a/plugins/airbrake-backend/api-report.md b/plugins/airbrake-backend/api-report.md index b9e9a50e0b..3ad74e2506 100644 --- a/plugins/airbrake-backend/api-report.md +++ b/plugins/airbrake-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // @public export interface AirbrakeConfig { @@ -21,6 +21,6 @@ export function extractAirbrakeConfig(config: Config): AirbrakeConfig; // @public export interface RouterOptions { airbrakeConfig: AirbrakeConfig; - logger: Logger_2; + logger: Logger; } ``` diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json index 2ee36eefb7..c8c87dd117 100644 --- a/plugins/airbrake-backend/package.json +++ b/plugins/airbrake-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake-backend", - "version": "0.2.0", + "version": "0.2.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -9,17 +9,20 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "scripts": { - "start": "backstage-cli backend:dev", - "build": "backstage-cli backend:build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@types/express": "*", "express": "^4.17.1", @@ -30,7 +33,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "supertest": "^6.1.6", diff --git a/plugins/airbrake/.eslintrc.js b/plugins/airbrake/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/airbrake/.eslintrc.js +++ b/plugins/airbrake/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md index 685ef4c0e7..374cdc7c7c 100644 --- a/plugins/airbrake/CHANGELOG.md +++ b/plugins/airbrake/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-airbrake +## 0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/dev-utils@0.2.25-next.0 + +## 0.3.1 + +### Patch Changes + +- 3c1d3cb07e: The Airbrake plugin installation instructions have been updated to work better and conform to how the frontend and backend plugins are supposed to be integrated into a Backstage instance. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/test-utils@0.3.0 + - @backstage/dev-utils@0.2.24 + ## 0.3.0 ### Minor Changes diff --git a/plugins/airbrake/README.md b/plugins/airbrake/README.md index d5ea9bb006..bc830c326d 100644 --- a/plugins/airbrake/README.md +++ b/plugins/airbrake/README.md @@ -20,7 +20,7 @@ The Airbrake plugin provides connectivity between Backstage and Airbrake (https: yarn add @backstage/plugin-airbrake-backend ``` -3. Add the `EntityAirbrakeContent` to `packages/app/src/components/catalog/EntityPage.tsx`: +3. Add the `EntityAirbrakeContent` to `packages/app/src/components/catalog/EntityPage.tsx` for all the entity pages you want Airbrake to be in: ```typescript jsx import { EntityAirbrakeContent } from '@backstage/plugin-airbrake'; @@ -32,40 +32,78 @@ The Airbrake plugin provides connectivity between Backstage and Airbrake (https: ); + + const websiteEntityPage = ( + + + + + + ); + + const defaultEntityPage = ( + + + + + + ); ``` -4. Setup the Backend code in `packages/backend/src/index.ts`: +4. Create `packages/backend/src/plugins/airbrake.ts` with these contents: ```typescript + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; import { - createRouter as createAirbrakeRouter, + createRouter, extractAirbrakeConfig, } from '@backstage/plugin-airbrake-backend'; - async function main() { - //... After const config = await loadBackendConfig({ ... - - const airbrakeRouter = await createAirbrakeRouter({ + export default async function createPlugin({ + logger, + config, + }: PluginEnvironment): Promise { + return createRouter({ logger, airbrakeConfig: extractAirbrakeConfig(config), }); - - const service = createServiceBuilder(module) - // ... Add the airbrakeRouter here - .addRouter('/api/airbrake', airbrakeRouter); } ``` -5. Add this config as a top level section in your `app-config.yaml`: +5. Setup the Backend code in `packages/backend/src/index.ts`: + + ```typescript + import airbrake from './plugins/airbrake'; + + async function main() { + //... After const createEnv = makeCreateEnv(config) ... + + const airbrakeEnv = useHotMemoize(module, () => createEnv('airbrake')); + + //... After const apiRouter = Router() ... + apiRouter.use('/airbrake', await airbrake(airbrakeEnv)); + } + ``` + +6. Add this config as a top level section in your `app-config.yaml`: ```yaml airbrake: apiKey: ${AIRBRAKE_API_KEY} ``` -6. Set an environment variable `AIRBRAKE_API_KEY` with your [API key](https://airbrake.io/docs/api/#authentication) +7. Set an environment variable `AIRBRAKE_API_KEY` with your [API key](https://airbrake.io/docs/api/#authentication) before starting Backstage backend. +8. Add the following annotation to the `catalog-info.yaml` for a repo you want to link to an Airbrake project: + + ```yaml + metadata: + annotations: + airbrake.io/project-id: '123456' + ``` + ## Local Development Start this plugin in standalone mode by running `yarn start` inside the plugin directory. This method of serving the plugin provides quicker diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 7342fdd839..52e1244230 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake", - "version": "0.3.0", + "version": "0.3.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,12 +23,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/dev-utils": "^0.2.23", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/test-utils": "^0.2.6", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", + "@backstage/test-utils": "^0.3.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -40,10 +40,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/app-defaults": "^0.1.9", - "@backstage/cli": "^0.14.1", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/app-defaults": "^0.2.1-next.0", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index ee14a17f9a..e8d09b5e65 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -18,23 +18,21 @@ import React from 'react'; import { EntityAirbrakeWidget } from './EntityAirbrakeWidget'; import exampleData from '../../api/mock/airbrakeGroupsApiMock.json'; import { - MockErrorApi, renderInTestApp, setupRequestMockHandlers, TestApiProvider, } from '@backstage/test-utils'; -import { createEntity } from '../../api'; import { airbrakeApiRef, + createEntity, localDiscoveryApi, MockAirbrakeApi, ProductionAirbrakeApi, } from '../../api'; -import { errorApiRef } from '@backstage/core-plugin-api'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -describe('EntityAirbrakeContent', () => { +describe('EntityAirbrakeWidget', () => { const worker = setupServer(); setupRequestMockHandlers(worker); @@ -52,15 +50,20 @@ describe('EntityAirbrakeContent', () => { } }); - it('states that the annotation is missing if no project ID annotation is provided', async () => { + it('states that the annotation is missing if no project ID annotation is provided but does not error', async () => { const widget = await renderInTestApp( - + , ); await expect( widget.findByText('Missing Annotation'), ).resolves.toBeInTheDocument(); + expect( + widget.queryByText(/.*Failed fetching Airbrake groups.*/), + ).not.toBeInTheDocument(); }); it('states that an error occurred if the API call fails', async () => { @@ -72,14 +75,10 @@ describe('EntityAirbrakeContent', () => { }, ), ); - const mockErrorApi = new MockErrorApi({ collect: true }); const widget = await renderInTestApp( , @@ -88,9 +87,10 @@ describe('EntityAirbrakeContent', () => { await expect( widget.findByText(/.*there was an issue communicating with Airbrake.*/), ).resolves.toBeInTheDocument(); - expect(mockErrorApi.getErrors().length).toBe(1); - expect(mockErrorApi.getErrors()[0].error.message).toStrictEqual( - 'Failed fetching Airbrake groups', - ); + expect( + widget.getByRole('heading', { + name: /.*Failed fetching Airbrake groups.*/, + }), + ).toBeInTheDocument(); }); }); diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx index b11116aeef..6c2e4b8686 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -14,10 +14,11 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Grid, Typography } from '@material-ui/core'; import { EmptyState, + ErrorPanel, InfoCard, MissingAnnotationEmptyState, Progress, @@ -25,7 +26,7 @@ import { import hash from 'object-hash'; import { makeStyles } from '@material-ui/core/styles'; import { BackstageTheme } from '@backstage/theme'; -import { ErrorApi, errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; import { airbrakeApiRef } from '../../api'; import useAsync from 'react-use/lib/useAsync'; import { AIRBRAKE_PROJECT_ID_ANNOTATION, useProjectId } from '../useProjectId'; @@ -40,55 +41,50 @@ export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { const classes = useStyles(); const projectId = useProjectId(entity); - const errorApi = useApi(errorApiRef); const airbrakeApi = useApi(airbrakeApiRef); - const { loading, value, error } = useAsync( - () => airbrakeApi.fetchGroups(projectId), - [airbrakeApi, projectId], - ); - - useEffect(() => { - if (error) { - errorApi.post(error); + const { loading, value, error } = useAsync(() => { + if (!projectId) { + return Promise.resolve(undefined); } - }, [error, errorApi]); - if (loading || !projectId || error) { + return airbrakeApi.fetchGroups(projectId); + }, [airbrakeApi, projectId]); + + if (!projectId) { return ( - - {loading && } - - {!loading && !projectId && ( - - )} - - {!loading && error && ( - - )} - + + ); + } else if (loading) { + return ; + } else if (value) { + return ( + + {value.groups?.map(group => ( + + {group.errors?.map(groupError => ( + + + {groupError.message} + + + ))} + + ))} + ); } return ( - - {value?.groups?.map(group => ( - - {group.errors?.map(groupError => ( - - - {groupError.message} - - - ))} - - ))} - + <> + {error && } + + ); }; diff --git a/plugins/allure/.eslintrc.js b/plugins/allure/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/allure/.eslintrc.js +++ b/plugins/allure/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md index 43e83e2d1e..6520fef7f4 100644 --- a/plugins/allure/CHANGELOG.md +++ b/plugins/allure/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-allure +## 0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.16 ### Patch Changes diff --git a/plugins/allure/package.json b/plugins/allure/package.json index eaa3013da8..ade58d7180 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-allure", "description": "A Backstage plugin that integrates with Allure", - "version": "0.1.16", + "version": "0.1.18-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,10 +25,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -40,10 +40,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx index 34d12aa032..e6355d861f 100644 --- a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx +++ b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx @@ -23,6 +23,7 @@ import { setupRequestMockHandlers, renderInTestApp, } from '@backstage/test-utils'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; describe('ExampleComponent', () => { const server = setupServer(); @@ -39,7 +40,15 @@ describe('ExampleComponent', () => { it('should render', async () => { const rendered = await renderInTestApp( - + + + , ); expect(rendered.getByText('Missing Annotation')).toBeInTheDocument(); diff --git a/plugins/analytics-module-ga/.eslintrc.js b/plugins/analytics-module-ga/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/analytics-module-ga/.eslintrc.js +++ b/plugins/analytics-module-ga/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md index ccf5ebd98c..fdf4f1704a 100644 --- a/plugins/analytics-module-ga/CHANGELOG.md +++ b/plugins/analytics-module-ga/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-analytics-module-ga +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.1-next.0 + +## 0.1.12 + +### Patch Changes + +- 123019673b: Added CSP instructions to README +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.11 ### Patch Changes diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index d71b66cd6f..f3d684dce3 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga", - "version": "0.1.11", + "version": "0.1.13-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,8 +25,8 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -38,10 +38,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/apache-airflow/.eslintrc.js b/plugins/apache-airflow/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/apache-airflow/.eslintrc.js +++ b/plugins/apache-airflow/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md index e86a6cef5d..f1e24ae6d5 100644 --- a/plugins/apache-airflow/CHANGELOG.md +++ b/plugins/apache-airflow/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-apache-airflow +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.1-next.0 + +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.8 ### Patch Changes diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json index b5045551bb..5e36dcca52 100644 --- a/plugins/apache-airflow/package.json +++ b/plugins/apache-airflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apache-airflow", - "version": "0.1.8", + "version": "0.1.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,8 +23,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -36,10 +36,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/api-docs/.eslintrc.js b/plugins/api-docs/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/api-docs/.eslintrc.js +++ b/plugins/api-docs/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 6bbbe9d056..f145c80222 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-api-docs +## 0.8.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@0.10.0-next.0 + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.8.1 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog@0.9.1 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.8.0 ### Minor Changes diff --git a/plugins/api-docs/dev/index.tsx b/plugins/api-docs/dev/index.tsx index 633a285b98..9975e91e98 100644 --- a/plugins/api-docs/dev/index.tsx +++ b/plugins/api-docs/dev/index.tsx @@ -53,8 +53,8 @@ createDevApp() items: mockEntities.slice(), }; }, - async getEntityByName(name: string) { - return mockEntities.find(e => e.metadata.name === name); + async getEntityByRef(ref: string) { + return mockEntities.find(e => e.metadata.name === ref); }, } as unknown as typeof catalogApiRef.T), }) diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 331845bd13..0edaa58731 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.8.0", + "version": "0.8.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,11 +34,11 @@ }, "dependencies": { "@asyncapi/react-component": "1.0.0-next.33", - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog": "^0.10.0-next.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -56,10 +56,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx index 16b335b0a7..bc413661a8 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx @@ -14,7 +14,11 @@ * limitations under the License. */ -import { Entity, RELATION_MEMBER_OF } from '@backstage/catalog-model'; +import { + Entity, + parseEntityRef, + RELATION_MEMBER_OF, +} from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/core-app-api'; import { TableColumn, TableProps } from '@backstage/core-components'; import { @@ -22,11 +26,13 @@ import { configApiRef, storageApiRef, } from '@backstage/core-plugin-api'; -import { CatalogTableRow } from '@backstage/plugin-catalog'; +import { + CatalogTableRow, + DefaultStarredEntitiesApi, +} from '@backstage/plugin-catalog'; import { CatalogApi, catalogApiRef, - DefaultStarredEntitiesApi, entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; @@ -58,11 +64,11 @@ describe('DefaultApiExplorerPage', () => { }), getLocationByRef: () => Promise.resolve({ id: 'id', type: 'url', target: 'url' }), - getEntityByName: async entityName => { + getEntityByRef: async entityRef => { return { apiVersion: 'backstage.io/v1alpha1', kind: 'User', - metadata: { name: entityName.name }, + metadata: { name: parseEntityRef(entityRef).name }, relations: [ { type: RELATION_MEMBER_OF, diff --git a/plugins/app-backend/.eslintrc.js b/plugins/app-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/app-backend/.eslintrc.js +++ b/plugins/app-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index baf9a928d8..c93408d321 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-app-backend +## 0.3.29-next.0 + +### Patch Changes + +- ab7cd7d70e: Do some groundwork for supporting the `better-sqlite3` driver, to maybe eventually replace `@vscode/sqlite3` (#9912) +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/config-loader@0.9.7-next.0 + +## 0.3.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.3.27 ### Patch Changes diff --git a/plugins/app-backend/api-report.md b/plugins/app-backend/api-report.md index 50d1b7bc34..e1b0c63469 100644 --- a/plugins/app-backend/api-report.md +++ b/plugins/app-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -23,7 +23,7 @@ export interface RouterOptions { database?: PluginDatabaseManager; disableConfigInjection?: boolean; // (undocumented) - logger: Logger_2; + logger: Logger; staticFallbackHandler?: express.Handler; } ``` diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 79682ca9de..3ff2b14a19 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-backend", "description": "A Backstage backend plugin that serves the Backstage frontend app", - "version": "0.3.27", + "version": "0.3.29-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,15 +33,15 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/config-loader": "^0.9.6", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/config-loader": "^0.9.7-next.0", "@backstage/config": "^0.1.15", "@backstage/types": "^0.1.3", "@types/express": "^4.17.6", "globby": "^11.0.0", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "fs-extra": "9.1.0", + "fs-extra": "10.0.1", "helmet": "^5.0.2", "knex": "^1.0.2", "lodash": "^4.17.21", @@ -50,8 +50,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.19", - "@backstage/cli": "^0.14.1", + "@backstage/backend-test-utils": "^0.1.21-next.0", + "@backstage/cli": "^0.15.2-next.0", "@backstage/types": "^0.1.3", "@types/supertest": "^2.0.8", "mock-fs": "^5.1.0", diff --git a/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts b/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts index 0230486444..95de905e7a 100644 --- a/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts +++ b/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts @@ -140,10 +140,9 @@ describe('StaticAssetsStore', () => { const updated = await database('static_assets_cache') .where({ path: 'old' }) .update({ - last_modified_at: - database.client.config.client === 'sqlite3' - ? database.raw(`datetime('now', '-3600 seconds')`) - : database.raw(`now() + interval '-3600 seconds'`), + last_modified_at: database.client.config.client.includes('sqlite3') + ? database.raw(`datetime('now', '-3600 seconds')`) + : database.raw(`now() + interval '-3600 seconds'`), }); expect(updated).toBe(1); diff --git a/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts b/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts index 6b393ce036..70d664307e 100644 --- a/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts +++ b/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts @@ -133,7 +133,7 @@ export class StaticAssetsStore implements StaticAssetProvider { .where( 'last_modified_at', '<=', - this.#db.client.config.client === 'sqlite3' + this.#db.client.config.client.includes('sqlite3') ? this.#db.raw(`datetime('now', ?)`, [`-${maxAgeSeconds} seconds`]) : this.#db.raw(`now() + interval '${-maxAgeSeconds} seconds'`), ) diff --git a/plugins/auth-backend/.eslintrc.js b/plugins/auth-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/auth-backend/.eslintrc.js +++ b/plugins/auth-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 5f85363fcc..6ee4b7d146 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-auth-backend +## 0.12.1-next.0 + +### Patch Changes + +- ab7cd7d70e: Do some groundwork for supporting the `better-sqlite3` driver, to maybe eventually replace `@vscode/sqlite3` (#9912) +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- bf95bb806c: Remove usages of now-removed `CatalogApi.getEntityByName` +- 3c2bc73901: Use `setupRequestMockHandlers` from `@backstage/backend-test-utils` +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/catalog-client@0.9.0-next.0 + - @backstage/plugin-auth-node@0.1.5-next.0 + +## 0.12.0 + +### Minor Changes + +- 0c8ba31d72: **BREAKING**: The `TokenFactory.issueToken` used by custom sign-in resolvers now ensures that the sub claim given is a full entity reference of the format `:/`. Any existing custom sign-in resolver functions that do not supply a full entity reference must be updated. + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + - @backstage/plugin-auth-node@0.1.4 + ## 0.11.0 ### Minor Changes diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 416da59b46..4227f1db0a 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -11,7 +11,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import express from 'express'; import { JsonValue } from '@backstage/types'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Profile } from 'passport'; @@ -85,7 +85,7 @@ export type AuthProviderFactoryOptions = { providerId: string; globalConfig: AuthProviderConfig; config: Config; - logger: Logger_2; + logger: Logger; tokenManager: TokenManager; tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; @@ -106,7 +106,7 @@ export interface AuthProviderRouteHandlers { export type AuthResolverContext = { tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; - logger: Logger_2; + logger: Logger; }; // Warning: (ae-missing-release-tag) "AuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -642,7 +642,7 @@ export interface RouterOptions { // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) - logger: Logger_2; + logger: Logger; // Warning: (ae-forgotten-export) The symbol "ProviderFactories" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/plugins/auth-backend/migrations/20210326100300_timestamptz.js b/plugins/auth-backend/migrations/20210326100300_timestamptz.js index 144f450380..79b839dc6a 100644 --- a/plugins/auth-backend/migrations/20210326100300_timestamptz.js +++ b/plugins/auth-backend/migrations/20210326100300_timestamptz.js @@ -21,7 +21,7 @@ */ exports.up = async function up(knex) { // Sqlite does not support alter column. - if (knex.client.config.client !== 'sqlite3') { + if (knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('signing_keys', table => { table .timestamp('created_at', { useTz: true, precision: 0 }) @@ -38,7 +38,7 @@ exports.up = async function up(knex) { */ exports.down = async function down(knex) { // Sqlite does not support alter column. - if (knex.client.config.client !== 'sqlite3') { + if (knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('signing_keys', table => { table .timestamp('created_at', { useTz: false, precision: 0 }) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 7a370ee8a9..5a1df93c93 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.11.0", + "version": "0.12.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,10 +33,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/plugin-auth-node": "^0.1.3", - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/plugin-auth-node": "^0.1.5-next.0", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@backstage/types": "^0.1.3", @@ -49,7 +49,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-session": "^1.17.1", - "fs-extra": "9.1.0", + "fs-extra": "10.0.1", "google-auth-library": "^7.6.1", "jose": "^1.27.1", "jwt-decode": "^3.1.0", @@ -76,8 +76,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/backend-test-utils": "^0.1.21-next.0", + "@backstage/cli": "^0.15.2-next.0", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/auth-backend/src/identity/TokenFactory.test.ts b/plugins/auth-backend/src/identity/TokenFactory.test.ts index f5f06f1209..4b1c1804df 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.test.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.test.ts @@ -18,6 +18,7 @@ import { MemoryKeyStore } from './MemoryKeyStore'; import { TokenFactory } from './TokenFactory'; import { getVoidLogger } from '@backstage/backend-common'; import { JWKS, JSONWebKey, JWT } from 'jose'; +import { stringifyEntityRef } from '@backstage/catalog-model'; const logger = getVoidLogger(); @@ -28,6 +29,12 @@ function jwtKid(jwt: string): string { return header.kid; } +const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: 'default', + name: 'JackFrost', +}); + describe('TokenFactory', () => { it('should issue valid tokens signed by a listed key', async () => { const keyDurationSeconds = 5; @@ -39,7 +46,7 @@ describe('TokenFactory', () => { }); await expect(factory.listPublicKeys()).resolves.toEqual({ keys: [] }); - const token = await factory.issueToken({ claims: { sub: 'foo' } }); + const token = await factory.issueToken({ claims: { sub: entityRef } }); const { keys } = await factory.listPublicKeys(); const keyStore = JWKS.asKeyStore({ @@ -53,7 +60,7 @@ describe('TokenFactory', () => { expect(payload).toEqual({ iss: 'my-issuer', aud: 'backstage', - sub: 'foo', + sub: entityRef, iat: expect.any(Number), exp: expect.any(Number), }); @@ -71,8 +78,12 @@ describe('TokenFactory', () => { logger, }); - const token1 = await factory.issueToken({ claims: { sub: 'foo' } }); - const token2 = await factory.issueToken({ claims: { sub: 'foo' } }); + const token1 = await factory.issueToken({ + claims: { sub: entityRef }, + }); + const token2 = await factory.issueToken({ + claims: { sub: entityRef }, + }); expect(jwtKid(token1)).toBe(jwtKid(token2)); await expect(factory.listPublicKeys()).resolves.toEqual({ @@ -89,7 +100,9 @@ describe('TokenFactory', () => { keys: [], }); - const token3 = await factory.issueToken({ claims: { sub: 'foo' } }); + const token3 = await factory.issueToken({ + claims: { sub: entityRef }, + }); expect(jwtKid(token3)).not.toBe(jwtKid(token2)); await expect(factory.listPublicKeys()).resolves.toEqual({ @@ -100,4 +113,20 @@ describe('TokenFactory', () => { ], }); }); + + it('should throw an error with a non entityRef sub claim', async () => { + const keyDurationSeconds = 5; + const factory = new TokenFactory({ + issuer: 'my-issuer', + keyStore: new MemoryKeyStore(), + keyDurationSeconds, + logger, + }); + + await expect(() => { + return factory.issueToken({ + claims: { sub: 'UserId' }, + }); + }).rejects.toThrowError(); + }); }); diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index 041a292d44..cb2a8b453d 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -19,6 +19,7 @@ import { JSONWebKey, JWK, JWS } from 'jose'; import { Logger } from 'winston'; import { v4 as uuid } from 'uuid'; import { DateTime } from 'luxon'; +import { parseEntityRef } from '@backstage/catalog-model'; const MS_IN_S = 1000; @@ -72,6 +73,15 @@ export class TokenFactory implements TokenIssuer { const iat = Math.floor(Date.now() / MS_IN_S); const exp = iat + this.keyDurationSeconds; + // Validate that the subject claim is a valid EntityRef + try { + parseEntityRef(sub); + } catch (error) { + throw new Error( + '"sub" claim provided by the auth resolver is not a valid EntityRef.', + ); + } + this.logger.info(`Issuing token for ${sub}, with entities ${ent ?? []}`); return JWS.sign({ iss, sub, aud, iat, exp, ent }, key, { diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index d2cee7bd89..b99c8a7615 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -26,7 +26,7 @@ import { CatalogIdentityClient } from './CatalogIdentityClient'; describe('CatalogIdentityClient', () => { const catalogApi: jest.Mocked = { getLocationById: jest.fn(), - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), getEntities: jest.fn(), addLocation: jest.fn(), removeLocationById: jest.fn(), diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index d948af2e05..502da913af 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -18,7 +18,7 @@ import { Logger } from 'winston'; import { ConflictError, NotFoundError } from '@backstage/errors'; import { CatalogApi } from '@backstage/catalog-client'; import { - EntityName, + CompoundEntityRef, parseEntityRef, RELATION_MEMBER_OF, stringifyEntityRef, @@ -96,7 +96,7 @@ export class CatalogIdentityClient { return null; } }) - .filter((ref): ref is EntityName => ref !== null); + .filter((ref): ref is CompoundEntityRef => ref !== null); const filter = resolvedEntityRefs.map(ref => ({ kind: ref.kind, diff --git a/plugins/auth-backend/src/providers/microsoft/provider.test.ts b/plugins/auth-backend/src/providers/microsoft/provider.test.ts index 2734878077..3a0fc5ad86 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.test.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.test.ts @@ -20,7 +20,7 @@ import { OAuthResult } from '../../lib/oauth'; import { getVoidLogger } from '@backstage/backend-common'; import { TokenIssuer } from '../../identity/types'; import { CatalogIdentityClient } from '../../lib/catalog'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index da96ae1a25..d9902b600e 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -15,7 +15,7 @@ */ import { Config, ConfigReader } from '@backstage/config'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import express from 'express'; import { Session } from 'express-session'; import { JWK, JWT } from 'jose'; diff --git a/plugins/auth-node/.eslintrc.js b/plugins/auth-node/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/auth-node/.eslintrc.js +++ b/plugins/auth-node/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index 7676ce7afa..dbb8316cf2 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-auth-node +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/backend-common@0.12.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 2becee849d..a302feca9e 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.1.3", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,8 +23,8 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "jose": "^1.27.1", @@ -32,7 +32,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "msw": "^0.35.0", "uuid": "^8.0.0" }, diff --git a/plugins/azure-devops-backend/.eslintrc.js b/plugins/azure-devops-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/azure-devops-backend/.eslintrc.js +++ b/plugins/azure-devops-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index 0156f92891..c841ec0e4d 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-azure-devops-backend +## 0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.3.6 ### Patch Changes diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index 579367e2dd..f9b8b75b82 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -10,7 +10,7 @@ import { Config } from '@backstage/config'; import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; import express from 'express'; import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PullRequest } from '@backstage/plugin-azure-devops-common'; import { PullRequestOptions } from '@backstage/plugin-azure-devops-common'; import { RepoBuild } from '@backstage/plugin-azure-devops-common'; @@ -22,7 +22,7 @@ import { WebApi } from 'azure-devops-node-api'; // // @public (undocumented) export class AzureDevOpsApi { - constructor(logger: Logger_2, webApi: WebApi); + constructor(logger: Logger, webApi: WebApi); // (undocumented) getAllTeams(): Promise; // (undocumented) @@ -96,7 +96,7 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; } // (No @packageDocumentation comment for this package) diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 206294c863..6a94a2b4d3 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.3.6", + "version": "0.3.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,7 +23,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/plugin-azure-devops-common": "^0.2.2", "@types/express": "^4.17.6", @@ -35,7 +35,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.6", "msw": "^0.35.0" diff --git a/plugins/azure-devops-common/.eslintrc.js b/plugins/azure-devops-common/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/azure-devops-common/.eslintrc.js +++ b/plugins/azure-devops-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/azure-devops-common/package.json b/plugins/azure-devops-common/package.json index 7810469bf9..24936f883a 100644 --- a/plugins/azure-devops-common/package.json +++ b/plugins/azure-devops-common/package.json @@ -32,7 +32,7 @@ "clean": "backstage-cli package clean" }, "devDependencies": { - "@backstage/cli": "^0.14.0" + "@backstage/cli": "^0.15.2-next.0" }, "files": [ "dist" diff --git a/plugins/azure-devops/.eslintrc.js b/plugins/azure-devops/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/azure-devops/.eslintrc.js +++ b/plugins/azure-devops/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index 7c0a533dd3..339539a7ea 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-azure-devops +## 0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.16 ### Patch Changes diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 57c1654527..fcfc97ce9d 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.1.16", + "version": "0.1.18-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,12 +30,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", "@backstage/plugin-azure-devops-common": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,10 +49,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/badges-backend/.eslintrc.js b/plugins/badges-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/badges-backend/.eslintrc.js +++ b/plugins/badges-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index ae93cdbe5a..a26b98d2e6 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-badges-backend +## 0.1.23-next.0 + +### Patch Changes + +- bf95bb806c: Remove usages of now-removed `CatalogApi.getEntityByName` +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/catalog-client@0.9.0-next.0 + +## 0.1.22 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + ## 0.1.21 ### Patch Changes diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index 94796ba665..d5e3fe3dcf 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges-backend", "description": "A Backstage backend plugin that generates README badges for your entities", - "version": "0.1.21", + "version": "0.1.23-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,9 +34,9 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@types/express": "^4.17.6", @@ -48,7 +48,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts index 56dbfb0655..1750a55be7 100644 --- a/plugins/badges-backend/src/service/router.test.ts +++ b/plugins/badges-backend/src/service/router.test.ts @@ -60,7 +60,7 @@ describe('createRouter', () => { catalog = { addLocation: jest.fn(), getEntities: jest.fn(), - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeLocationById: jest.fn(), @@ -103,7 +103,7 @@ describe('createRouter', () => { describe('GET /entity/:namespace/:kind/:name/badge-specs', () => { it('returns all badge specs for entity', async () => { - catalog.getEntityByName.mockResolvedValueOnce(entity); + catalog.getEntityByRef.mockResolvedValueOnce(entity); badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]); badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); @@ -115,8 +115,8 @@ describe('createRouter', () => { expect(response.status).toEqual(200); expect(response.text).toEqual(JSON.stringify([badge], null, 2)); - expect(catalog.getEntityByName).toHaveBeenCalledTimes(1); - expect(catalog.getEntityByName).toHaveBeenCalledWith( + expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1); + expect(catalog.getEntityByRef).toHaveBeenCalledWith( { namespace: 'default', kind: 'service', @@ -142,7 +142,7 @@ describe('createRouter', () => { describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => { it('returns badge for entity', async () => { - catalog.getEntityByName.mockResolvedValueOnce(entity); + catalog.getEntityByRef.mockResolvedValueOnce(entity); const image = '...'; badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image); @@ -154,8 +154,8 @@ describe('createRouter', () => { expect(response.status).toEqual(200); expect(response.body).toEqual(Buffer.from(image)); - expect(catalog.getEntityByName).toHaveBeenCalledTimes(1); - expect(catalog.getEntityByName).toHaveBeenCalledWith( + expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1); + expect(catalog.getEntityByRef).toHaveBeenCalledWith( { namespace: 'default', kind: 'service', @@ -179,7 +179,7 @@ describe('createRouter', () => { }); it('returns badge spec for entity', async () => { - catalog.getEntityByName.mockResolvedValueOnce(entity); + catalog.getEntityByRef.mockResolvedValueOnce(entity); badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); const url = '/entity/default/service/test/badge/test-badge?format=json'; @@ -192,7 +192,7 @@ describe('createRouter', () => { describe('Errors', () => { it('returns 404 for unknown entities', async () => { - catalog.getEntityByName.mockResolvedValue(undefined); + catalog.getEntityByRef.mockResolvedValue(undefined); async function testUrl(url: string) { const response = await request(app).get(url); expect(response.status).toEqual(404); diff --git a/plugins/badges-backend/src/service/router.ts b/plugins/badges-backend/src/service/router.ts index 196ddf36da..b7211a0aad 100644 --- a/plugins/badges-backend/src/service/router.ts +++ b/plugins/badges-backend/src/service/router.ts @@ -46,7 +46,7 @@ export async function createRouter( router.get('/entity/:namespace/:kind/:name/badge-specs', async (req, res) => { const { namespace, kind, name } = req.params; - const entity = await catalog.getEntityByName( + const entity = await catalog.getEntityByRef( { namespace, kind, name }, { token: getBearerToken(req.headers.authorization), @@ -84,7 +84,7 @@ export async function createRouter( '/entity/:namespace/:kind/:name/badge/:badgeId', async (req, res) => { const { namespace, kind, name, badgeId } = req.params; - const entity = await catalog.getEntityByName( + const entity = await catalog.getEntityByRef( { namespace, kind, name }, { token: getBearerToken(req.headers.authorization), diff --git a/plugins/badges/.eslintrc.js b/plugins/badges/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/badges/.eslintrc.js +++ b/plugins/badges/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index e624dc36e9..df3b511cb7 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-badges +## 0.2.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.2.25 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.2.24 ### Patch Changes diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 612b9f3b2d..c058989e94 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges", "description": "A Backstage plugin that generates README badges for your entities", - "version": "0.2.24", + "version": "0.2.26-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -46,10 +46,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/bazaar-backend/.eslintrc.js b/plugins/bazaar-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/bazaar-backend/.eslintrc.js +++ b/plugins/bazaar-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md index 50dc8b2ad3..e161516d5f 100644 --- a/plugins/bazaar-backend/CHANGELOG.md +++ b/plugins/bazaar-backend/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-bazaar-backend +## 0.1.13-next.0 + +### Patch Changes + +- ab7cd7d70e: Do some groundwork for supporting the `better-sqlite3` driver, to maybe eventually replace `@vscode/sqlite3` (#9912) +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/backend-test-utils@0.1.21-next.0 + +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/backend-test-utils@0.1.20 + ## 0.1.11 ### Patch Changes diff --git a/plugins/bazaar-backend/api-report.md b/plugins/bazaar-backend/api-report.md index 89f6da5305..b50994b15d 100644 --- a/plugins/bazaar-backend/api-report.md +++ b/plugins/bazaar-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -22,7 +22,7 @@ export interface RouterOptions { // (undocumented) database: PluginDatabaseManager; // (undocumented) - logger: Logger_2; + logger: Logger; } // (No @packageDocumentation comment for this package) diff --git a/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js b/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js index 03ed0a8d05..9dd8097ae2 100644 --- a/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js +++ b/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js @@ -15,7 +15,7 @@ */ exports.up = async function up(knex) { - if (knex.client.config.client === 'sqlite3') { + if (knex.client.config.client.includes('sqlite3')) { await knex.schema.dropTable('metadata'); await knex.schema.createTable('metadata', table => { table.increments('id').comment('Automatically generated unique ID'); @@ -93,7 +93,7 @@ exports.up = async function up(knex) { }; exports.down = async function down(knex) { - if (knex.client.config.client === 'sqlite3') { + if (knex.client.config.client.includes('sqlite3')) { await knex.schema.dropTable('metadata'); await knex.schema.createTable('metadata', table => { table diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index cb596770d4..6224e6d869 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar-backend", - "version": "0.1.11", + "version": "0.1.13-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,8 +23,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/backend-test-utils": "^0.1.19", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/backend-test-utils": "^0.1.21-next.0", "@backstage/config": "^0.1.15", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -34,7 +34,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1" + "@backstage/cli": "^0.15.2-next.0" }, "files": [ "dist", diff --git a/plugins/bazaar/.eslintrc.js b/plugins/bazaar/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/bazaar/.eslintrc.js +++ b/plugins/bazaar/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index df72f76d6a..1ad604fd9d 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-bazaar +## 0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.15.2-next.0 + - @backstage/plugin-catalog@0.10.0-next.0 + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/catalog-client@0.9.0-next.0 + +## 0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog@0.9.1 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/cli@0.15.0 + ## 0.1.15 ### Patch Changes diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index a38cd1cd16..6e44067bd7 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.1.15", + "version": "0.1.17-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,13 +24,13 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", - "@backstage/cli": "^0.14.1", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog": "^0.10.0-next.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,8 +47,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/dev-utils": "^0.2.23", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/dev-utils": "^0.2.25-next.0", "@testing-library/jest-dom": "^5.10.1", "cross-fetch": "^3.1.5" }, diff --git a/plugins/bitrise/.eslintrc.js b/plugins/bitrise/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/bitrise/.eslintrc.js +++ b/plugins/bitrise/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index 8af11aa8a4..26ce2d20b1 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-bitrise +## 0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.27 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 228eed3e9b..16b58ba129 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitrise", "description": "A Backstage plugin that integrates towards Bitrise", - "version": "0.1.27", + "version": "0.1.29-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,10 +24,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,10 +43,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/catalog-backend-module-aws/.eslintrc.js b/plugins/catalog-backend-module-aws/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/catalog-backend-module-aws/.eslintrc.js +++ b/plugins/catalog-backend-module-aws/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index 749ab3a230..8e15c16546 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.1.2-next.0 + +### Patch Changes + +- f115a7f8fd: Added `AwsS3DiscoveryProcessor`, which was moved here from `@backstage/plugin-catalog-backend` where it previously resided. +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-catalog-backend@0.24.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.1.1 + +### Patch Changes + +- 83a83381b0: Use the new `processingResult` export from the catalog backend +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/plugin-catalog-backend@0.23.0 + ## 0.1.0 ### Minor Changes diff --git a/plugins/catalog-backend-module-aws/api-report.md b/plugins/catalog-backend-module-aws/api-report.md index 1cab6555fe..876562fc8d 100644 --- a/plugins/catalog-backend-module-aws/api-report.md +++ b/plugins/catalog-backend-module-aws/api-report.md @@ -5,9 +5,11 @@ ```ts import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; +import { CatalogProcessorParser } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; +import { UrlReader } from '@backstage/backend-common'; // @public export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { @@ -15,7 +17,7 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; }, ): AwsOrganizationCloudAccountProcessor; // (undocumented) @@ -27,4 +29,18 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { emit: CatalogProcessorEmit, ): Promise; } + +// @public +export class AwsS3DiscoveryProcessor implements CatalogProcessor { + constructor(reader: UrlReader); + // (undocumented) + getProcessorName(): string; + // (undocumented) + readLocation( + location: LocationSpec, + optional: boolean, + emit: CatalogProcessorEmit, + parser: CatalogProcessorParser, + ): Promise; +} ``` diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index ebe77263ac..72324483d9 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", "description": "A Backstage catalog backend module that helps integrate towards AWS", - "version": "0.1.0", + "version": "0.1.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,19 +33,22 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-backend": "^0.22.0", + "@backstage/plugin-catalog-backend": "^0.24.0-next.0", "@backstage/types": "^0.1.3", "aws-sdk": "^2.840.0", "lodash": "^4.17.21", + "p-limit": "^3.0.2", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/lodash": "^4.14.151", - "aws-sdk-mock": "^5.2.1" + "aws-sdk-mock": "^5.2.1", + "yaml": "^1.9.2" }, "files": [ "dist", diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts index e5e72d5fa8..1018e49a41 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts @@ -20,7 +20,7 @@ import { CatalogProcessor, CatalogProcessorEmit, LocationSpec, - results, + processingResult, } from '@backstage/plugin-catalog-backend'; import AWS, { Credentials, Organizations } from 'aws-sdk'; import { Account, ListAccountsResponse } from 'aws-sdk/clients/organizations'; @@ -112,7 +112,7 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { return true; }) .forEach(entity => { - emit(results.entity(location, entity)); + emit(processingResult.entity(location, entity)); }); return true; diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.test.ts b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.test.ts similarity index 80% rename from plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.test.ts rename to plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.test.ts index 064a2ad724..6aab7b2bec 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.test.ts @@ -13,14 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getVoidLogger, UrlReaders } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { AwsS3DiscoveryProcessor } from './AwsS3DiscoveryProcessor'; -import { CatalogProcessorEntityResult, CatalogProcessorResult } from './types'; -import { defaultEntityDataParser } from './util/parse'; +import { + CatalogProcessorEntityResult, + CatalogProcessorResult, + processingResult, +} from '@backstage/plugin-catalog-backend'; import AWSMock from 'aws-sdk-mock'; import aws from 'aws-sdk'; import path from 'path'; +import YAML from 'yaml'; AWSMock.setSDKInstance(aws); const object: aws.S3.Types.Object = { @@ -36,10 +41,7 @@ AWSMock.mock( 'getObject', Buffer.from( require('fs').readFileSync( - path.resolve( - __dirname, - '__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt', - ), + path.resolve(__dirname, '__fixtures__/awsS3-mock-object.txt'), ), ), ); @@ -61,7 +63,17 @@ describe('readLocation', () => { it('should load from url', async () => { const generated = (await new Promise(emit => - processor.readLocation(spec, false, emit, defaultEntityDataParser), + processor.readLocation( + spec, + false, + emit, + async function* r({ data, location }) { + yield processingResult.entity( + location, + YAML.parse(data.toString('utf8')) as any, + ); + }, + ), )) as CatalogProcessorEntityResult; expect(generated.type).toBe('entity'); expect(generated.location).toEqual({ diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts similarity index 84% rename from plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.ts rename to plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts index 0dc44cc157..0390defb2a 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts @@ -16,16 +16,22 @@ import { UrlReader } from '@backstage/backend-common'; import { isError } from '@backstage/errors'; -import limiterFactory from 'p-limit'; -import * as result from './results'; import { CatalogProcessor, CatalogProcessorEmit, CatalogProcessorParser, LocationSpec, -} from './types'; + processingResult, +} from '@backstage/plugin-catalog-backend'; +import limiterFactory from 'p-limit'; -/** @public */ +/** + * A processor for automatic discovery of entities from S3 buckets. Handles the + * `s3-discovery` location type, and target bucket URLs e.g. on the form + * `https://testbucket.s3.us-east-2.amazonaws.com`. + * + * @public + */ export class AwsS3DiscoveryProcessor implements CatalogProcessor { constructor(private readonly reader: UrlReader) {} @@ -58,10 +64,10 @@ export class AwsS3DiscoveryProcessor implements CatalogProcessor { if (isError(error) && error.name === 'NotFoundError') { if (!optional) { - emit(result.notFoundError(location, message)); + emit(processingResult.notFoundError(location, message)); } } else { - emit(result.generalError(location, message)); + emit(processingResult.generalError(location, message)); } } return true; diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt b/plugins/catalog-backend-module-aws/src/processors/__fixtures__/awsS3-mock-object.txt similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt rename to plugins/catalog-backend-module-aws/src/processors/__fixtures__/awsS3-mock-object.txt diff --git a/plugins/catalog-backend-module-aws/src/processors/index.ts b/plugins/catalog-backend-module-aws/src/processors/index.ts index fa858aa744..8718e77a6f 100644 --- a/plugins/catalog-backend-module-aws/src/processors/index.ts +++ b/plugins/catalog-backend-module-aws/src/processors/index.ts @@ -15,3 +15,4 @@ */ export { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAccountProcessor'; +export { AwsS3DiscoveryProcessor } from './AwsS3DiscoveryProcessor'; diff --git a/plugins/catalog-backend-module-azure/.eslintrc.js b/plugins/catalog-backend-module-azure/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/catalog-backend-module-azure/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md new file mode 100644 index 0000000000..f6f876ca9f --- /dev/null +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -0,0 +1,14 @@ +# @backstage/plugin-catalog-backend-module-azure + +## 0.1.0-next.0 + +### Minor Changes + +- 66ba5d9023: Added package, moving out azure specific functionality from the catalog-backend + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-catalog-backend@0.24.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 diff --git a/plugins/catalog-backend-module-azure/README.md b/plugins/catalog-backend-module-azure/README.md new file mode 100644 index 0000000000..f460a7e672 --- /dev/null +++ b/plugins/catalog-backend-module-azure/README.md @@ -0,0 +1,8 @@ +# Catalog Backend Module for Azure + +This is an extension module to the plugin-catalog-backend plugin, providing extensions targeted at Azure cloud offerings. + +## Getting started + +See [Backstage documentation](https://backstage.io/docs/integrations/azure/org) for details on how to install +and configure the plugin. diff --git a/plugins/catalog-backend-module-azure/api-report.md b/plugins/catalog-backend-module-azure/api-report.md new file mode 100644 index 0000000000..139db6aab5 --- /dev/null +++ b/plugins/catalog-backend-module-azure/api-report.md @@ -0,0 +1,35 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-azure" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; +import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; +import { Config } from '@backstage/config'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; +import { Logger } from 'winston'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +// @public +export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { + constructor(options: { + integrations: ScmIntegrationRegistry; + logger: Logger; + }); + // (undocumented) + static fromConfig( + config: Config, + options: { + logger: Logger; + }, + ): AzureDevOpsDiscoveryProcessor; + // (undocumented) + getProcessorName(): string; + // (undocumented) + readLocation( + location: LocationSpec, + _optional: boolean, + emit: CatalogProcessorEmit, + ): Promise; +} +``` diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json new file mode 100644 index 0000000000..91e421633d --- /dev/null +++ b/plugins/catalog-backend-module-azure/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-azure", + "description": "A Backstage catalog backend module that helps integrate towards Azure", + "version": "0.1.0-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-azure" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" + }, + "dependencies": { + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-catalog-backend": "^0.24.0-next.0", + "@backstage/types": "^0.1.3", + "lodash": "^4.17.21", + "msw": "^0.35.0", + "node-fetch": "^2.6.7", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^0.1.21-next.0", + "@backstage/cli": "^0.15.2-next.0", + "@types/lodash": "^4.14.151" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts rename to plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.test.ts index a406feaa74..e6eed8d976 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.test.ts @@ -16,14 +16,14 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { codeSearch } from './azure'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { AzureDevOpsDiscoveryProcessor, parseUrl, } from './AzureDevOpsDiscoveryProcessor'; -import { LocationSpec } from './types'; +import { codeSearch } from './lib'; -jest.mock('./azure'); +jest.mock('./lib'); const mockCodeSearch = codeSearch as jest.MockedFunction; describe('AzureDevOpsDiscoveryProcessor', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.ts similarity index 95% rename from plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts rename to plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.ts index bc74621f1d..bdc7255f44 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.ts @@ -19,10 +19,14 @@ import { ScmIntegrationRegistry, ScmIntegrations, } from '@backstage/integration'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '@backstage/plugin-catalog-backend'; import { Logger } from 'winston'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; -import { codeSearch } from './azure'; +import { codeSearch } from './lib'; /** * Extracts repositories out of an Azure DevOps org. @@ -102,7 +106,7 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { for (const file of files) { emit( - results.location({ + processingResult.location({ type: 'url', target: `${baseUrl}/${org}/${project}/_git/${file.repository.name}?path=${file.path}`, // Not all locations may actually exist, since the user defined them as a wildcard pattern. diff --git a/plugins/catalog-backend-module-azure/src/index.ts b/plugins/catalog-backend-module-azure/src/index.ts new file mode 100644 index 0000000000..f6c61e52a5 --- /dev/null +++ b/plugins/catalog-backend-module-azure/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * A Backstage catalog backend module that helps integrate towards Azure + * + * @packageDocumentation + */ + +export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor'; diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts rename to plugins/catalog-backend-module-azure/src/lib/azure.test.ts index 67cc120bab..16f5067197 100644 --- a/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; +import { setupServer } from 'msw/node'; import { codeSearch, CodeSearchResponse } from './azure'; describe('azure', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/azure.ts b/plugins/catalog-backend-module-azure/src/lib/azure.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/azure/azure.ts rename to plugins/catalog-backend-module-azure/src/lib/azure.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/index.ts b/plugins/catalog-backend-module-azure/src/lib/index.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/azure/index.ts rename to plugins/catalog-backend-module-azure/src/lib/index.ts diff --git a/packages/core-app-api/src/apis/implementations/auth/auth0/index.ts b/plugins/catalog-backend-module-azure/src/setupTests.ts similarity index 92% rename from packages/core-app-api/src/apis/implementations/auth/auth0/index.ts rename to plugins/catalog-backend-module-azure/src/setupTests.ts index 9daed7d13e..d3232290a7 100644 --- a/packages/core-app-api/src/apis/implementations/auth/auth0/index.ts +++ b/plugins/catalog-backend-module-azure/src/setupTests.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default as Auth0Auth } from './Auth0Auth'; +export {}; diff --git a/plugins/catalog-backend-module-gitlab/.eslintrc.js b/plugins/catalog-backend-module-gitlab/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md new file mode 100644 index 0000000000..09f6df6fe4 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -0,0 +1,14 @@ +# @backstage/plugin-catalog-backend-module-gitlab + +## 0.1.0-next.0 + +### Minor Changes + +- 66ba5d9023: Added package, moving out GitLab specific functionality from the catalog-backend + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-catalog-backend@0.24.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 diff --git a/plugins/catalog-backend-module-gitlab/README.md b/plugins/catalog-backend-module-gitlab/README.md new file mode 100644 index 0000000000..3e1881eb76 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/README.md @@ -0,0 +1,8 @@ +# Catalog Backend Module for GitLab + +This is an extension module to the plugin-catalog-backend plugin, providing extensions targeted at GitLab offerings. + +## Getting started + +See [Backstage documentation](https://backstage.io/docs/integrations/gitlab/discovery) for details on how to install +and configure the plugin. diff --git a/plugins/catalog-backend-module-gitlab/api-report.md b/plugins/catalog-backend-module-gitlab/api-report.md new file mode 100644 index 0000000000..ec10f80939 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/api-report.md @@ -0,0 +1,30 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-gitlab" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; +import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; +import { Config } from '@backstage/config'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; +import { Logger } from 'winston'; + +// @public +export class GitLabDiscoveryProcessor implements CatalogProcessor { + // (undocumented) + static fromConfig( + config: Config, + options: { + logger: Logger; + }, + ): GitLabDiscoveryProcessor; + // (undocumented) + getProcessorName(): string; + // (undocumented) + readLocation( + location: LocationSpec, + _optional: boolean, + emit: CatalogProcessorEmit, + ): Promise; +} +``` diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json new file mode 100644 index 0000000000..7073291d2b --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-gitlab", + "description": "A Backstage catalog backend module that helps integrate towards GitLab", + "version": "0.1.0-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-gitlab" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" + }, + "dependencies": { + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-catalog-backend": "^0.24.0-next.0", + "@backstage/types": "^0.1.3", + "lodash": "^4.17.21", + "msw": "^0.35.0", + "node-fetch": "^2.6.7", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^0.1.21-next.0", + "@backstage/cli": "^0.15.2-next.0", + "@types/lodash": "^4.14.151" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.test.ts rename to plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts index 8261690e8f..dc4ffb7251 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; -import { GitLabDiscoveryProcessor, parseUrl } from './GitLabDiscoveryProcessor'; -import { setupServer } from 'msw/node'; +import { ConfigReader } from '@backstage/config'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { rest } from 'msw'; -import { GitLabProject } from './gitlab'; -import { LocationSpec } from './types'; +import { setupServer } from 'msw/node'; +import { GitLabDiscoveryProcessor, parseUrl } from './GitLabDiscoveryProcessor'; +import { GitLabProject } from './lib'; const server = setupServer(); diff --git a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts similarity index 92% rename from plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts rename to plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts index b534d50044..01de1f12a0 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts @@ -14,20 +14,24 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; -import { - ScmIntegrationRegistry, - ScmIntegrations, -} from '@backstage/integration'; -import { Logger } from 'winston'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; -import { GitLabClient, GitLabProject, paginated } from './gitlab'; import { CacheClient, CacheManager, PluginCacheManager, } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; +import { + ScmIntegrationRegistry, + ScmIntegrations, +} from '@backstage/integration'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '@backstage/plugin-catalog-backend'; +import { Logger } from 'winston'; +import { GitLabClient, GitLabProject, paginated } from './lib'; /** * Extracts repositories out of an GitLab instance. @@ -95,12 +99,12 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { page: 1, }); - const result: Result = { + const res: Result = { scanned: 0, matches: [], }; for await (const project of projects) { - result.scanned++; + res.scanned++; if (project.archived) { continue; @@ -110,14 +114,14 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { continue; } - result.matches.push(project); + res.matches.push(project); } - for (const project of result.matches) { + for (const project of res.matches) { const project_branch = branch === '*' ? project.default_branch : branch; emit( - results.location({ + processingResult.location({ type: 'url', // The format expected by the GitLabUrlReader: // https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath @@ -133,7 +137,7 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); this.logger.debug( - `Read ${result.scanned} GitLab repositories in ${duration} seconds`, + `Read ${res.scanned} GitLab repositories in ${duration} seconds`, ); return true; diff --git a/plugins/catalog-backend-module-gitlab/src/index.ts b/plugins/catalog-backend-module-gitlab/src/index.ts new file mode 100644 index 0000000000..e2a08509fb --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * A Backstage catalog backend module that helps integrate towards GitLab + * + * @packageDocumentation + */ + +export { GitLabDiscoveryProcessor } from './GitLabDiscoveryProcessor'; diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts rename to plugins/catalog-backend-module-gitlab/src/lib/client.test.ts index b249137740..e50ad34734 100644 --- a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { ConfigReader } from '@backstage/config'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { readGitLabIntegrationConfig } from '@backstage/integration'; import { getVoidLogger } from '@backstage/backend-common'; import { rest } from 'msw'; import { setupServer, SetupServerApi } from 'msw/node'; - import { GitLabClient, paginated } from './client'; const server = setupServer(); diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts rename to plugins/catalog-backend-module-gitlab/src/lib/client.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/index.ts b/plugins/catalog-backend-module-gitlab/src/lib/index.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/gitlab/index.ts rename to plugins/catalog-backend-module-gitlab/src/lib/index.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/types.ts b/plugins/catalog-backend-module-gitlab/src/lib/types.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/gitlab/types.ts rename to plugins/catalog-backend-module-gitlab/src/lib/types.ts diff --git a/plugins/catalog-backend-module-gitlab/src/setupTests.ts b/plugins/catalog-backend-module-gitlab/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export {}; diff --git a/plugins/catalog-backend-module-ldap/.eslintrc.js b/plugins/catalog-backend-module-ldap/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/catalog-backend-module-ldap/.eslintrc.js +++ b/plugins/catalog-backend-module-ldap/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index e05e4936a8..6933b4e6c5 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.4.0-next.0 + +### Minor Changes + +- 9461f73643: **BREAKING**: Added a `schedule` field to `LdapOrgEntityProvider.fromConfig`, which is required. If you want to retain the old behavior of scheduling the provider manually, you can set it to the string value `'manual'`. But you may want to leverage the ability to instead pass in the recurring task schedule information directly. This will allow you to simplify your backend setup code to not need an intermediate variable and separate scheduling code at the bottom. + + All things said, a typical setup might now look as follows: + + ```diff + // packages/backend/src/plugins/catalog.ts + +import { Duration } from 'luxon'; + +import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap'; + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); + + // The target parameter below needs to match the ldap.providers.target + + // value specified in your app-config. + + builder.addEntityProvider( + + LdapOrgEntityProvider.fromConfig(env.config, { + + id: 'our-ldap-master', + + target: 'ldaps://ds.example.net', + + logger: env.logger, + + schedule: env.scheduler.createScheduledTaskRunner({ + + frequency: Duration.fromObject({ minutes: 60 }), + + timeout: Duration.fromObject({ minutes: 15 }), + + }), + + }), + + ); + ``` + +### Patch Changes + +- f751e84572: Ignore search referrals instead of throwing an error. +- Updated dependencies + - @backstage/backend-tasks@0.2.0-next.0 + - @backstage/plugin-catalog-backend@0.24.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.3.15 + +### Patch Changes + +- 83a83381b0: Use the new `processingResult` export from the catalog backend +- 66aa05c23c: Fixed bug in Catalog LDAP module to acknowledge page events to continue receiving entries if pagePause=true +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/plugin-catalog-backend@0.23.0 + ## 0.3.14 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index f6b2475e65..edeef4cb82 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -12,9 +12,10 @@ import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { GroupEntity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { SearchEntry } from 'ldapjs'; import { SearchOptions } from 'ldapjs'; +import { TaskRunner } from '@backstage/backend-tasks'; import { UserEntity } from '@backstage/catalog-model'; // @public @@ -75,10 +76,10 @@ export const LDAP_UUID_ANNOTATION = 'backstage.io/ldap-uuid'; // @public export class LdapClient { - constructor(client: Client, logger: Logger_2); + constructor(client: Client, logger: Logger); // (undocumented) static create( - logger: Logger_2, + logger: Logger, target: string, bind?: BindConfig, ): Promise; @@ -97,7 +98,7 @@ export class LdapOrgEntityProvider implements EntityProvider { constructor(options: { id: string; provider: LdapProviderConfig; - logger: Logger_2; + logger: Logger; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; }); @@ -106,24 +107,28 @@ export class LdapOrgEntityProvider implements EntityProvider { // (undocumented) static fromConfig( configRoot: Config, - options: { - id: string; - target: string; - userTransformer?: UserTransformer; - groupTransformer?: GroupTransformer; - logger: Logger_2; - }, + options: LdapOrgEntityProviderOptions, ): LdapOrgEntityProvider; // (undocumented) getProviderName(): string; - read(): Promise; + read(options?: { logger?: Logger }): Promise; +} + +// @public +export interface LdapOrgEntityProviderOptions { + groupTransformer?: GroupTransformer; + id: string; + logger: Logger; + schedule: 'manual' | TaskRunner; + target: string; + userTransformer?: UserTransformer; } // @public export class LdapOrgReaderProcessor implements CatalogProcessor { constructor(options: { providers: LdapProviderConfig[]; - logger: Logger_2; + logger: Logger; groupTransformer?: GroupTransformer; userTransformer?: UserTransformer; }); @@ -131,7 +136,7 @@ export class LdapOrgReaderProcessor implements CatalogProcessor { static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; groupTransformer?: GroupTransformer; userTransformer?: UserTransformer; }, @@ -180,7 +185,7 @@ export function readLdapOrg( options: { groupTransformer?: GroupTransformer; userTransformer?: UserTransformer; - logger: Logger_2; + logger: Logger; }, ): Promise<{ users: UserEntity[]; diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index dc2f56aad6..a09f93536f 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend module that helps integrate towards LDAP", - "version": "0.3.14", + "version": "0.4.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,18 +33,20 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-tasks": "^0.2.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-backend": "^0.22.0", + "@backstage/plugin-catalog-backend": "^0.24.0-next.0", "@backstage/types": "^0.1.3", "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", "lodash": "^4.17.21", + "uuid": "^8.0.0", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/lodash": "^4.14.151" }, "files": [ diff --git a/plugins/catalog-backend-module-ldap/src/ldap/client.ts b/plugins/catalog-backend-module-ldap/src/ldap/client.ts index 414d043d3d..e778b0cfe4 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/client.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/client.ts @@ -95,7 +95,7 @@ export class LdapClient { } res.on('searchReference', () => { - reject(new Error('Unable to handle referral')); + this.logger.warn('Received unsupported search referral'); }); res.on('searchEntry', entry => { @@ -154,7 +154,7 @@ export class LdapClient { } res.on('searchReference', () => { - reject(new Error('Unable to handle referral')); + this.logger.warn('Received unsupported search referral'); }); res.on('searchEntry', entry => { diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts index 4c5bd2e3bd..bad3941767 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { TaskRunner } from '@backstage/backend-tasks'; import { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, @@ -25,6 +26,7 @@ import { EntityProviderConnection, } from '@backstage/plugin-catalog-backend'; import { merge } from 'lodash'; +import * as uuid from 'uuid'; import { Logger } from 'winston'; import { GroupTransformer, @@ -36,6 +38,59 @@ import { UserTransformer, } from '../ldap'; +/** + * Options for {@link LdapOrgEntityProvider}. + * + * @public + */ +export interface LdapOrgEntityProviderOptions { + /** + * A unique, stable identifier for this provider. + * + * @example "production" + */ + id: string; + + /** + * The target that this provider should consume. + * + * Should exactly match the "target" field of one of the "ldap.providers" + * configuration entries. + * + * @example "ldaps://ds-read.example.net" + */ + target: string; + + /** + * The logger to use. + */ + logger: Logger; + + /** + * The refresh schedule to use. + * + * @remarks + * + * If you pass in 'manual', you are responsible for calling the `read` method + * manually at some interval. + * + * But more commonly you will pass in the result of + * {@link @backstage/backend-tasks#PluginTaskScheduler.createScheduledTaskRunner} + * to enable automatic scheduling of tasks. + */ + schedule: 'manual' | TaskRunner; + + /** + * The function that transforms a user entry in LDAP to an entity. + */ + userTransformer?: UserTransformer; + + /** + * The function that transforms a group entry in LDAP to an entity. + */ + groupTransformer?: GroupTransformer; +} + /** * Reads user and group entries out of an LDAP service, and provides them as * User and Group entities for the catalog. @@ -49,35 +104,11 @@ import { */ export class LdapOrgEntityProvider implements EntityProvider { private connection?: EntityProviderConnection; + private scheduleFn?: () => Promise; static fromConfig( configRoot: Config, - options: { - /** - * A unique, stable identifier for this provider. - * - * @example "production" - */ - id: string; - /** - * The target that this provider should consume. - * - * Should exactly match the "target" field of one of the "ldap.providers" - * configuration entries. - * - * @example "ldaps://ds-read.example.net" - */ - target: string; - /** - * The function that transforms a user entry in LDAP to an entity. - */ - userTransformer?: UserTransformer; - /** - * The function that transforms a group entry in LDAP to an entity. - */ - groupTransformer?: GroupTransformer; - logger: Logger; - }, + options: LdapOrgEntityProviderOptions, ): LdapOrgEntityProvider { // TODO(freben): Deprecate the old catalog.processors.ldapOrg config const config = @@ -101,13 +132,17 @@ export class LdapOrgEntityProvider implements EntityProvider { target: options.target, }); - return new LdapOrgEntityProvider({ + const result = new LdapOrgEntityProvider({ id: options.id, provider, userTransformer: options.userTransformer, groupTransformer: options.groupTransformer, logger, }); + + result.schedule(options.schedule); + + return result; } constructor( @@ -128,18 +163,20 @@ export class LdapOrgEntityProvider implements EntityProvider { /** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */ async connect(connection: EntityProviderConnection) { this.connection = connection; + await this.scheduleFn?.(); } /** - * Runs one complete ingestion loop. Call this method regularly at some - * appropriate cadence. + * Runs one single complete ingestion. This is only necessary if you use + * manual scheduling. */ - async read() { + async read(options?: { logger?: Logger }) { if (!this.connection) { throw new Error('Not initialized'); } - const { markReadComplete } = trackProgress(this.options.logger); + const logger = options?.logger ?? this.options.logger; + const { markReadComplete } = trackProgress(logger); // Be lazy and create the client each time; even though it's pretty // inefficient, we usually only do this once per entire refresh loop and @@ -157,7 +194,7 @@ export class LdapOrgEntityProvider implements EntityProvider { { groupTransformer: this.options.groupTransformer, userTransformer: this.options.userTransformer, - logger: this.options.logger, + logger, }, ); @@ -173,6 +210,32 @@ export class LdapOrgEntityProvider implements EntityProvider { markCommitComplete(); } + + private schedule(schedule: LdapOrgEntityProviderOptions['schedule']) { + if (schedule === 'manual') { + return; + } + + this.scheduleFn = async () => { + const id = `${this.getProviderName()}:refresh`; + await schedule.run({ + id, + fn: async () => { + const logger = this.options.logger.child({ + class: LdapOrgEntityProvider.prototype.constructor.name, + taskId: id, + taskInstanceId: uuid.v4(), + }); + + try { + await this.read({ logger }); + } catch (error) { + logger.error(error); + } + }, + }); + }; + } } // Helps wrap the timing and logging behaviors diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts index 6c31d5a27d..bec24a1744 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts @@ -28,7 +28,7 @@ import { CatalogProcessor, CatalogProcessorEmit, LocationSpec, - results, + processingResult, } from '@backstage/plugin-catalog-backend'; /** @@ -119,10 +119,10 @@ export class LdapOrgReaderProcessor implements CatalogProcessor { // Done! for (const group of groups) { - emit(results.entity(location, group)); + emit(processingResult.entity(location, group)); } for (const user of users) { - emit(results.entity(location, user)); + emit(processingResult.entity(location, user)); } return true; diff --git a/plugins/catalog-backend-module-ldap/src/processors/index.ts b/plugins/catalog-backend-module-ldap/src/processors/index.ts index 96e1a49cbb..5ed0095c3e 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/index.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/index.ts @@ -15,4 +15,5 @@ */ export { LdapOrgEntityProvider } from './LdapOrgEntityProvider'; +export type { LdapOrgEntityProviderOptions } from './LdapOrgEntityProvider'; export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor'; diff --git a/plugins/catalog-backend-module-msgraph/.eslintrc.js b/plugins/catalog-backend-module-msgraph/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/catalog-backend-module-msgraph/.eslintrc.js +++ b/plugins/catalog-backend-module-msgraph/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index d2e9facfb5..7b8a358517 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.2.19-next.0 + +### Patch Changes + +- 3c2bc73901: Use `setupRequestMockHandlers` from `@backstage/backend-test-utils` +- Updated dependencies + - @backstage/plugin-catalog-backend@0.24.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.2.18 + +### Patch Changes + +- c820a49426: add config option `groupExpand` to allow expanding a single relationship +- 83a83381b0: Use the new `processingResult` export from the catalog backend +- 4bc61a64e2: add documentation for config options `userGroupMemberSearch` and `groupSearch` +- f9bb6aa0aa: add `userExpand` config option to allow expanding a single relationship +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/plugin-catalog-backend@0.23.0 + ## 0.2.17 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index 0e088d3a4e..a253359471 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -35,17 +35,38 @@ catalog: # the App registration in the Microsoft Azure Portal. clientId: ${MICROSOFT_GRAPH_CLIENT_ID} clientSecret: ${MICROSOFT_GRAPH_CLIENT_SECRET_TOKEN} + # Optional parameter to include the expanded resource or collection referenced + # by a single relationship (navigation property) in your results. + # Only one relationship can be expanded in a single request. + # See https://docs.microsoft.com/en-us/graph/query-parameters#expand-parameter + # Can be combined with userGroupMember[...] instead of userFilter. + userExpand: manager # Optional filter for user, see Microsoft Graph API for the syntax # See https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties # and for the syntax https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter # This and userGroupMemberFilter are mutually exclusive, only one can be specified userFilter: accountEnabled eq true and userType eq 'member' # Optional filter for users, use group membership to get users. + # (Filtered groups and fetch their members.) # This and userFilter are mutually exclusive, only one can be specified + # See https://docs.microsoft.com/en-us/graph/search-query-parameter userGroupMemberFilter: "displayName eq 'Backstage Users'" + # Optional parameter to include the expanded resource or collection referenced + # by a single relationship (navigation property) in your results. + # Only one relationship can be expanded in a single request. + # See https://docs.microsoft.com/en-us/graph/query-parameters#expand-parameter + # Can be combined with userGroupMember[...] instead of userFilter. + groupExpand: member + # Optional search for users, use group membership to get users. + # (Search for groups and fetch their members.) + # This and userFilter are mutually exclusive, only one can be specified + userGroupMemberSearch: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' # Optional filter for group, see Microsoft Graph API for the syntax # See https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties groupFilter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified') + # Optional search for groups, see Microsoft Graph API for the syntax + # See https://docs.microsoft.com/en-us/graph/search-query-parameter + groupSearch: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' ``` `userFilter` and `userGroupMemberFilter` are mutually exclusive, only one can be provided. If both are provided, an error will be thrown. diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 7ad603e6ca..f7d0f112ad 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -10,7 +10,7 @@ import { EntityProvider } from '@backstage/plugin-catalog-backend'; import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { GroupEntity } from '@backstage/catalog-model'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import * as msal from '@azure/msal-node'; import { Response as Response_2 } from 'node-fetch'; @@ -78,7 +78,10 @@ export class MicrosoftGraphClient { userId: string, maxSize: number, ): Promise; - getUserProfile(userId: string): Promise; + getUserProfile( + userId: string, + query?: ODataQuery, + ): Promise; getUsers(query?: ODataQuery): AsyncIterable; requestApi( path: string, @@ -97,7 +100,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { constructor(options: { id: string; provider: MicrosoftGraphProviderConfig; - logger: Logger_2; + logger: Logger; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; @@ -110,7 +113,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { options: { id: string; target: string; - logger: Logger_2; + logger: Logger; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; @@ -125,7 +128,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { constructor(options: { providers: MicrosoftGraphProviderConfig[]; - logger: Logger_2; + logger: Logger; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; @@ -134,7 +137,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; @@ -158,9 +161,10 @@ export type MicrosoftGraphProviderConfig = { clientId: string; clientSecret: string; userFilter?: string; - userExpand?: string[]; + userExpand?: string; userGroupMemberFilter?: string; userGroupMemberSearch?: string; + groupExpand?: string; groupFilter?: string; groupSearch?: string; }; @@ -172,7 +176,7 @@ export function normalizeEntityName(name: string): string; export type ODataQuery = { search?: string; filter?: string; - expand?: string[]; + expand?: string; select?: string[]; }; @@ -191,16 +195,17 @@ export function readMicrosoftGraphOrg( client: MicrosoftGraphClient, tenantId: string, options: { - userExpand?: string[]; + userExpand?: string; userFilter?: string; userGroupMemberSearch?: string; userGroupMemberFilter?: string; + groupExpand?: string; groupSearch?: string; groupFilter?: string; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; - logger: Logger_2; + logger: Logger; }, ): Promise<{ users: UserEntity[]; diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 5005a16c5b..5de78b3ec4 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", - "version": "0.2.17", + "version": "0.2.19-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,9 +34,9 @@ }, "dependencies": { "@azure/msal-node": "^1.1.0", - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", - "@backstage/plugin-catalog-backend": "^0.22.0", + "@backstage/plugin-catalog-backend": "^0.24.0-next.0", "@microsoft/microsoft-graph-types": "^2.6.0", "@types/node-fetch": "^2.5.12", "lodash": "^4.17.21", @@ -46,9 +46,9 @@ "qs": "^6.9.4" }, "devDependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/backend-test-utils": "^0.1.21-next.0", + "@backstage/cli": "^0.15.2-next.0", "@types/lodash": "^4.14.151", "msw": "^0.35.0" }, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts index 6413a97c15..467b03733e 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts @@ -15,7 +15,7 @@ */ import * as msal from '@azure/msal-node'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { MicrosoftGraphClient } from './client'; @@ -85,7 +85,7 @@ describe('MicrosoftGraphClient', () => { const response = await client.requestApi('users', { filter: 'test eq true', - expand: ['children'], + expand: 'children', select: ['id', 'children'], }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts index db749a536e..cba5d9f640 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts @@ -38,7 +38,7 @@ export type ODataQuery = { /** * specifies the related resources or media streams to be included in line with retrieved resources */ - expand?: string[]; + expand?: string; /** * request a specific set of properties for each entity or complex type */ @@ -155,7 +155,7 @@ export class MicrosoftGraphClient { $search: query?.search, $filter: query?.filter, $select: query?.select?.join(','), - $expand: query?.expand?.join(','), + $expand: query?.expand, }, { addQueryPrefix: true, @@ -203,10 +203,14 @@ export class MicrosoftGraphClient { * * @public * @param userId - The unique identifier for the `User` resource + * @param query - OData Query {@link ODataQuery} * */ - async getUserProfile(userId: string): Promise { - const response = await this.requestApi(`users/${userId}`); + async getUserProfile( + userId: string, + query?: ODataQuery, + ): Promise { + const response = await this.requestApi(`users/${userId}`, query); if (response.status !== 200) { await this.handleError('user profile', response); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts index efbbca1f5c..cfc5c1cbb0 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts @@ -53,7 +53,9 @@ describe('readMicrosoftGraphConfig', () => { clientId: 'clientId', clientSecret: 'clientSecret', authority: 'https://login.example.com/', + userExpand: 'manager', userFilter: 'accountEnabled eq true', + groupExpand: 'member', groupFilter: 'securityEnabled eq false', }, ], @@ -66,7 +68,9 @@ describe('readMicrosoftGraphConfig', () => { clientId: 'clientId', clientSecret: 'clientSecret', authority: 'https://login.example.com', + userExpand: 'manager', userFilter: 'accountEnabled eq true', + groupExpand: 'member', groupFilter: 'securityEnabled eq false', }, ]; diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index 6809144f80..b7fbcfb8a9 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -57,7 +57,7 @@ export type MicrosoftGraphProviderConfig = { * * E.g. "manager" */ - userExpand?: string[]; + userExpand?: string; /** * The filter to apply to extract users by groups memberships. * @@ -70,6 +70,12 @@ export type MicrosoftGraphProviderConfig = { * E.g. "\"displayName:-team\"" would only match groups which contain '-team' */ userGroupMemberSearch?: string; + /** + * The "expand" argument to apply to groups. + * + * E.g. "member" + */ + groupExpand?: string; /** * The filter to apply to extract groups. * @@ -106,6 +112,8 @@ export function readMicrosoftGraphConfig( const tenantId = providerConfig.getString('tenantId'); const clientId = providerConfig.getString('clientId'); const clientSecret = providerConfig.getString('clientSecret'); + + const userExpand = providerConfig.getOptionalString('userExpand'); const userFilter = providerConfig.getOptionalString('userFilter'); const userGroupMemberFilter = providerConfig.getOptionalString( 'userGroupMemberFilter', @@ -113,6 +121,7 @@ export function readMicrosoftGraphConfig( const userGroupMemberSearch = providerConfig.getOptionalString( 'userGroupMemberSearch', ); + const groupExpand = providerConfig.getOptionalString('groupExpand'); const groupFilter = providerConfig.getOptionalString('groupFilter'); const groupSearch = providerConfig.getOptionalString('groupSearch'); @@ -133,9 +142,11 @@ export function readMicrosoftGraphConfig( tenantId, clientId, clientSecret, + userExpand, userFilter, userGroupMemberFilter, userGroupMemberSearch, + groupExpand, groupFilter, groupSearch, }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index b0273e1f58..e3d6897d66 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -118,7 +118,7 @@ describe('read microsoft graph', () => { expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); }); - it('should read users with custom transformer', async () => { + it('should read users with userExpand and custom transformer', async () => { async function* getExampleUsers() { yield { id: 'userid', @@ -133,6 +133,7 @@ describe('read microsoft graph', () => { ); const { users } = await readMicrosoftGraphUsers(client, { + userExpand: 'manager', userFilter: 'accountEnabled eq true', transformer: async () => ({ apiVersion: 'backstage.io/v1alpha1', @@ -154,6 +155,7 @@ describe('read microsoft graph', () => { expect(client.getUsers).toBeCalledTimes(1); expect(client.getUsers).toBeCalledWith({ + expand: 'manager', filter: 'accountEnabled eq true', }); expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); @@ -227,12 +229,14 @@ describe('read microsoft graph', () => { expect(client.getGroupMembers).toBeCalledWith('groupid'); expect(client.getUserProfile).toBeCalledTimes(1); - expect(client.getUserProfile).toBeCalledWith('userid'); + expect(client.getUserProfile).toBeCalledWith('userid', { + expand: undefined, + }); expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); }); - it('should read users with custom transformer', async () => { + it('should read users with userExpand, groupExpand and custom transformer', async () => { async function* getExampleGroups() { yield { id: 'groupid', @@ -266,7 +270,9 @@ describe('read microsoft graph', () => { ); const { users } = await readMicrosoftGraphUsersInGroups(client, { + userExpand: 'manager', userGroupMemberFilter: 'securityEnabled eq true', + groupExpand: 'member', transformer: async () => ({ apiVersion: 'backstage.io/v1alpha1', kind: 'User', @@ -287,13 +293,16 @@ describe('read microsoft graph', () => { expect(client.getGroups).toBeCalledTimes(1); expect(client.getGroups).toBeCalledWith({ + expand: 'member', filter: 'securityEnabled eq true', }); expect(client.getGroupMembers).toBeCalledTimes(1); expect(client.getGroupMembers).toBeCalledWith('groupid'); expect(client.getUserProfile).toBeCalledTimes(1); - expect(client.getUserProfile).toBeCalledWith('userid'); + expect(client.getUserProfile).toBeCalledWith('userid', { + expand: 'manager', + }); expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); }); @@ -446,6 +455,100 @@ describe('read microsoft graph', () => { // expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120); }); + it('should read groups with groupExpand', async () => { + async function* getExampleGroups() { + yield { + id: 'groupid', + displayName: 'Group Name', + description: 'Group Description', + mail: 'group@example.com', + }; + } + + async function* getExampleGroupMembers(): AsyncIterable { + yield { + '@odata.type': '#microsoft.graph.group', + id: 'childgroupid', + }; + yield { + '@odata.type': '#microsoft.graph.user', + id: 'userid', + }; + } + + client.getGroups.mockImplementation(getExampleGroups); + client.getGroupMembers.mockImplementation(getExampleGroupMembers); + client.getOrganization.mockResolvedValue({ + id: 'tenantid', + displayName: 'Organization Name', + }); + client.getGroupPhotoWithSizeLimit.mockResolvedValue( + 'data:image/jpeg;base64,...', + ); + + const { groups, groupMember, groupMemberOf, rootGroup } = + await readMicrosoftGraphGroups(client, 'tenantid', { + groupExpand: 'member', + groupFilter: 'securityEnabled eq false', + }); + + const expectedRootGroup = group({ + metadata: { + annotations: { + 'graph.microsoft.com/tenant-id': 'tenantid', + }, + name: 'organization_name', + description: 'Organization Name', + }, + spec: { + type: 'root', + profile: { + displayName: 'Organization Name', + }, + children: [], + }, + }); + expect(groups).toEqual([ + expectedRootGroup, + group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'groupid', + }, + name: 'group_name', + description: 'Group Description', + }, + spec: { + type: 'team', + profile: { + displayName: 'Group Name', + email: 'group@example.com', + // TODO: Loading groups photos doesn't work right now as Microsoft + // Graph doesn't allows this yet + /* picture: 'data:image/jpeg;base64,...',*/ + }, + children: [], + }, + }), + ]); + expect(rootGroup).toEqual(expectedRootGroup); + expect(groupMember.get('groupid')).toEqual(new Set(['childgroupid'])); + expect(groupMemberOf.get('userid')).toEqual(new Set(['groupid'])); + expect(groupMember.get('organization_name')).toEqual(new Set()); + + expect(client.getGroups).toBeCalledTimes(1); + expect(client.getGroups).toBeCalledWith({ + expand: 'member', + filter: 'securityEnabled eq false', + }); + expect(client.getGroupMembers).toBeCalledTimes(1); + expect(client.getGroupMembers).toBeCalledWith('groupid'); + // TODO: Loading groups photos doesn't work right now as Microsoft Graph + // doesn't allows this yet + // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); + // expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120); + }); + it('should read security groups', async () => { async function* getExampleGroups() { yield { @@ -634,6 +737,14 @@ describe('read microsoft graph', () => { }; } + async function getExampleUserProfile(userId: string) { + return { + id: userId, + displayName: 'User Name', + mail: 'user.name@example.com', + }; + } + async function* getExampleGroups() { yield { id: 'groupid', @@ -686,7 +797,7 @@ describe('read microsoft graph', () => { }); }); - it('should read users using userFilter', async () => { + it('should read users using userExpand and userFilter', async () => { client.getOrganization.mockResolvedValue({ id: 'tenantid', displayName: 'Organization Name', @@ -705,12 +816,14 @@ describe('read microsoft graph', () => { await readMicrosoftGraphOrg(client, 'tenantid', { logger: getVoidLogger(), + userExpand: 'manager', userFilter: 'accountEnabled eq true', groupFilter: 'securityEnabled eq false', }); expect(client.getUsers).toBeCalledTimes(1); expect(client.getUsers).toBeCalledWith({ + expand: 'manager', filter: 'accountEnabled eq true', }); expect(client.getGroups).toBeCalledTimes(1); @@ -719,13 +832,14 @@ describe('read microsoft graph', () => { }); }); - it('should read users using userGroupMemberFilter', async () => { + it('should read users using userExpand and userGroupMemberFilter', async () => { client.getOrganization.mockResolvedValue({ id: 'tenantid', displayName: 'Organization Name', }); client.getUsers.mockImplementation(getExampleUsers); + client.getUserProfile.mockImplementation(getExampleUserProfile); client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', ); @@ -750,6 +864,8 @@ describe('read microsoft graph', () => { expect(client.getGroups).toBeCalledWith({ filter: 'securityEnabled eq false', }); + expect(client.getUserProfile).toBeCalledTimes(1); + expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); }); }); }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index f862fd5ace..ed6ca76e68 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -85,7 +85,7 @@ export async function readMicrosoftGraphUsers( client: MicrosoftGraphClient, options: { userFilter?: string; - userExpand?: string[]; + userExpand?: string; transformer?: UserTransformer; logger: Logger; }, @@ -137,8 +137,10 @@ export async function readMicrosoftGraphUsers( export async function readMicrosoftGraphUsersInGroups( client: MicrosoftGraphClient, options: { + userExpand?: string; userGroupMemberSearch?: string; userGroupMemberFilter?: string; + groupExpand?: string; transformer?: UserTransformer; logger: Logger; }, @@ -149,15 +151,16 @@ export async function readMicrosoftGraphUsersInGroups( const limiter = limiterFactory(10); - const transformer = options?.transformer ?? defaultUserTransformer; + const transformer = options.transformer ?? defaultUserTransformer; const userGroupMemberPromises: Promise[] = []; const userPromises: Promise[] = []; const groupMemberUsers: Set = new Set(); for await (const group of client.getGroups({ - search: options?.userGroupMemberSearch, - filter: options?.userGroupMemberFilter, + expand: options.groupExpand, + search: options.userGroupMemberSearch, + filter: options.userGroupMemberFilter, })) { // Process all groups in parallel, otherwise it can take quite some time userGroupMemberPromises.push( @@ -186,7 +189,9 @@ export async function readMicrosoftGraphUsersInGroups( let user; let userPhoto; try { - user = await client.getUserProfile(userId); + user = await client.getUserProfile(userId, { + expand: options.userExpand, + }); } catch (e) { options.logger.warn(`Unable to load user for ${userId}`); } @@ -326,8 +331,9 @@ export async function readMicrosoftGraphGroups( client: MicrosoftGraphClient, tenantId: string, options?: { - groupSearch?: string; + groupExpand?: string; groupFilter?: string; + groupSearch?: string; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; }, @@ -354,6 +360,7 @@ export async function readMicrosoftGraphGroups( const promises: Promise[] = []; for await (const group of client.getGroups({ + expand: options?.groupExpand, search: options?.groupSearch, filter: options?.groupFilter, })) { @@ -506,10 +513,11 @@ export async function readMicrosoftGraphOrg( client: MicrosoftGraphClient, tenantId: string, options: { - userExpand?: string[]; + userExpand?: string; userFilter?: string; userGroupMemberSearch?: string; userGroupMemberFilter?: string; + groupExpand?: string; groupSearch?: string; groupFilter?: string; userTransformer?: UserTransformer; diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts index 4db3a9b1a0..9037bd8728 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -19,7 +19,7 @@ import { CatalogProcessor, CatalogProcessorEmit, LocationSpec, - results, + processingResult, } from '@backstage/plugin-catalog-backend'; import { Logger } from 'winston'; import { @@ -109,6 +109,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { userFilter: provider.userFilter, userGroupMemberFilter: provider.userGroupMemberFilter, userGroupMemberSearch: provider.userGroupMemberSearch, + groupExpand: provider.groupExpand, groupFilter: provider.groupFilter, groupSearch: provider.groupSearch, userTransformer: this.userTransformer, @@ -125,10 +126,10 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { // Done! for (const group of groups) { - emit(results.entity(location, group)); + emit(processingResult.entity(location, group)); } for (const user of users) { - emit(results.entity(location, user)); + emit(processingResult.entity(location, user)); } return true; diff --git a/plugins/catalog-backend/.eslintrc.js b/plugins/catalog-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/catalog-backend/.eslintrc.js +++ b/plugins/catalog-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index d80acc6d23..664f58ffe5 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,117 @@ # @backstage/plugin-catalog-backend +## 0.24.0-next.0 + +### Minor Changes + +- 66ba5d9023: **BREAKING**: Removed `GitLabDiscoveryProcessor`, which now instead should be imported from `@backstage/plugin-catalog-backend-module-gitlab`. NOTE THAT this processor was part of the default set of processors in the catalog backend, and if you are a user of discovery on GitLab, you MUST now add it manually in the catalog initialization code of your backend. + + ```diff + // In packages/backend/src/plugins/catalog.ts + +import { GitLabDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-gitlab'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); + + builder.addProcessor( + + GitLabDiscoveryProcessor.fromConfig(env.config, { logger: env.logger }) + + ); + ``` + + **BREAKING**: Removed `AzureDevOpsDiscoveryProcessor`, which now instead should be imported from `@backstage/plugin-catalog-backend-module-azure`. This processor was not part of the set of default processors. If you were using it, you should already have a reference to it in your backend code and only need to update the import. + +- f115a7f8fd: **BREAKING**: Removed `AwsS3DiscoveryProcessor`, which now instead should be imported from `@backstage/plugin-catalog-backend-module-aws`. +- 55150919ed: - **BREAKING**: Support for `backstage.io/v1beta2` Software Templates has been removed. Please migrate your legacy templates to the new `scaffolder.backstage.io/v1beta3` `apiVersion` by following the [migration guide](https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3) + +### Patch Changes + +- ab7cd7d70e: Do some groundwork for supporting the `better-sqlite3` driver, to maybe eventually replace `@vscode/sqlite3` (#9912) +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- 616f02ade2: support Bitbucket Cloud's code search to discover catalog files (multiple per repo, Location entities for existing files only) +- e421d77536: **BREAKING**: + + - Removed the previously deprecated `runPeriodically` export. Please use the `@backstage/backend-tasks` package instead, or copy [the actual implementation](https://github.com/backstage/backstage/blob/02875d4d56708c60f86f6b0a5b3da82e24988354/plugins/catalog-backend/src/util/runPeriodically.ts#L29) into your own code if you explicitly do not want coordination of task runs across your worker nodes. + - Removed the previously deprecated `CatalogProcessorLocationResult.optional` field. Please set the corresponding `LocationSpec.presence` field to `'optional'` instead. + - Related to the previous point, the `processingResult.location` function no longer has a second boolean `optional` argument. Please set the corresponding `LocationSpec.presence` field to `'optional'` instead. + - Removed the previously deprecated `StaticLocationProcessor`. It has not been in use for some time; its functionality is covered by `ConfigLocationEntityProvider` instead. + +- 3c2bc73901: Use `setupRequestMockHandlers` from `@backstage/backend-test-utils` +- c1168bb440: Fixed display of the location in the log message that is printed when entity envelope validation fails. +- b1aacbf96a: Applied the fix for the `/alpha` entry point resolution that was part of the `v0.70.1` release of Backstage. +- 3e54f6c436: Use `@backstage/plugin-search-common` package instead of `@backstage/search-common`. +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-scaffolder-common@0.3.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/plugin-catalog-common@0.2.2-next.0 + - @backstage/plugin-search-common@0.3.1-next.0 + - @backstage/catalog-client@0.9.0-next.0 + - @backstage/plugin-permission-node@0.5.4-next.0 + +## 0.23.1 + +### Patch Changes + +- Marked `GithubMultiOrgReaderProcessor` as stable, as it was moved to `/alpha` by mistake. +- Fixed runtime resolution of the `/alpha` entry point. +- Updated dependencies + - @backstage/backend-common@0.12.1 + - @backstage/catalog-model@0.12.1 + - @backstage/plugin-catalog-common@0.2.1 + +## 0.23.0 + +### Minor Changes + +- 0c9cf2822d: **Breaking**: Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-backend/alpha` instead of `@backstage/plugin-catalog-backend`. + + - `catalogConditions` + - `createCatalogPolicyDecision` + - `permissionRules` + - `createCatalogPermissionRule` + +- 862e416239: **Breaking**: Removed `entityRef` from `CatalogProcessorRelationResult`. The field is not used by the catalog and relation information is already available inside the `reation` property. +- c85292b768: **Breaking**: Removed optional `handleError()` from `CatalogProcessor`. This optional method is never called by the catalog processing engine and can therefore be removed. + +### Patch Changes + +- 83a83381b0: **DEPRECATED**: The `results` export, and instead adding `processingResult` with the same shape and purpose. +- 83a83381b0: Internal restructuring to collect the various provider files in a `modules` folder while waiting to be externalized +- fc6d31b5c3: Deprecated the `BitbucketRepositoryParser` type. +- 022507c860: A `DefaultCatalogCollatorFactory`, which works with the new stream-based + search indexing subsystem, is now available. The `DefaultCatalogCollator` will + continue to be available for those unable to upgrade to the stream-based + `@backstage/search-backend-node` (and related packages), however it is now + marked as deprecated and will be removed in a future version. + + To upgrade this plugin and the search indexing subsystem in one go, check + [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for necessary changes to your search backend plugin configuration. + +- ab7b6cb7b1: **DEPRECATION**: Moved the `CatalogEntityDocument` to `@backstage/plugin-catalog-common` and deprecated the export from `@backstage/plugin-catalog-backend`. + + A new `type` field has also been added to `CatalogEntityDocument` as a replacement for `componentType`, which is now deprecated. Both fields are still present and should be set to the same value in order to avoid issues with indexing. + + Any search customizations need to be updated to use this new `type` field instead, including any custom frontend filters, custom frontend result components, custom search decorators, or non-default Catalog collator implementations. + +- cb09096607: Tweaked the wording of the "does not have a location" errors to include the actual missing annotation name, to help users better in fixing their inputs. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- b753d22a56: **DEPRECATION**: Deprecated the `RefreshIntervalFunction` and `createRandomRefreshInterval` in favour of the `ProcessingIntervalFunction` and `createRandomProcessingInterval` type and method respectively. Please migrate to use the new names. + + **DEPRECATION**: Deprecated the `setRefreshInterval` and `setRefreshIntervalSeconds` methods on the `CatalogBuilder` for the new `setProcessingInterval` and `setProcessingIntervalSeconds` methods. Please migrate to use the new names. + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + - @backstage/plugin-catalog-common@0.2.0 + - @backstage/integration@0.8.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-permission-node@0.5.3 + - @backstage/search-common@0.3.0 + - @backstage/plugin-scaffolder-common@0.2.3 + ## 0.22.0 ### Minor Changes diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 9b4db418b3..2a007fc52c 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -7,22 +7,22 @@ import { BitbucketIntegration } from '@backstage/integration'; import { CatalogApi } from '@backstage/catalog-client'; +import { CatalogEntityDocument as CatalogEntityDocument_2 } from '@backstage/plugin-catalog-common'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { ConditionalPolicyDecision } from '@backstage/plugin-permission-node'; import { Conditions } from '@backstage/plugin-permission-node'; import { Config } from '@backstage/config'; -import { DocumentCollator } from '@backstage/search-common'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; import { EntityPolicy } from '@backstage/catalog-model'; import express from 'express'; import { GetEntitiesRequest } from '@backstage/catalog-client'; import { GithubCredentialsProvider } from '@backstage/integration'; import { GitHubIntegrationConfig } from '@backstage/integration'; -import { IndexableDocument } from '@backstage/search-common'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Location as Location_2 } from '@backstage/catalog-client'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { PermissionCondition } from '@backstage/plugin-permission-common'; @@ -30,6 +30,7 @@ import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Readable } from 'stream'; import { Router } from 'express'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TokenManager } from '@backstage/backend-common'; @@ -96,56 +97,29 @@ export class AnnotateScmSlugEntityProcessor implements CatalogProcessor { preProcessEntity(entity: Entity, location: LocationSpec): Promise; } -// @public (undocumented) -export class AwsS3DiscoveryProcessor implements CatalogProcessor { - constructor(reader: UrlReader); - // (undocumented) - getProcessorName(): string; - // (undocumented) - readLocation( - location: LocationSpec, - optional: boolean, - emit: CatalogProcessorEmit, - parser: CatalogProcessorParser, - ): Promise; -} - -// @public -export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { - constructor(options: { - integrations: ScmIntegrationRegistry; - logger: Logger_2; - }); - // (undocumented) - static fromConfig( - config: Config, - options: { - logger: Logger_2; - }, - ): AzureDevOpsDiscoveryProcessor; - // (undocumented) - getProcessorName(): string; - // (undocumented) - readLocation( - location: LocationSpec, - _optional: boolean, - emit: CatalogProcessorEmit, - ): Promise; -} - // @public (undocumented) export class BitbucketDiscoveryProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - parser?: BitbucketRepositoryParser; - logger: Logger_2; + parser?: (options: { + integration: BitbucketIntegration; + target: string; + presence?: 'optional' | 'required'; + logger: Logger; + }) => AsyncIterable; + logger: Logger; }); // (undocumented) static fromConfig( config: Config, options: { - parser?: BitbucketRepositoryParser; - logger: Logger_2; + parser?: (options: { + integration: BitbucketIntegration; + target: string; + presence?: 'optional' | 'required'; + logger: Logger; + }) => AsyncIterable; + logger: Logger; }, ): BitbucketDiscoveryProcessor; // (undocumented) @@ -158,11 +132,12 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { ): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) export type BitbucketRepositoryParser = (options: { integration: BitbucketIntegration; target: string; - logger: Logger_2; + presence?: 'optional' | 'required'; + logger: Logger; }) => AsyncIterable; // @public (undocumented) @@ -209,11 +184,17 @@ export class CatalogBuilder { key: string, resolver: PlaceholderResolver, ): CatalogBuilder; + setProcessingInterval( + processingInterval: ProcessingIntervalFunction, + ): CatalogBuilder; + setProcessingIntervalSeconds(seconds: number): CatalogBuilder; + // @deprecated setRefreshInterval(refreshInterval: RefreshIntervalFunction): CatalogBuilder; + // @deprecated setRefreshIntervalSeconds(seconds: number): CatalogBuilder; } -// @public +// @alpha export const catalogConditions: Conditions<{ hasAnnotation: PermissionRule< Entity, @@ -239,23 +220,12 @@ export const catalogConditions: Conditions<{ >; }>; -// @public (undocumented) -export interface CatalogEntityDocument extends IndexableDocument { - // (undocumented) - componentType: string; - // (undocumented) - kind: string; - // (undocumented) - lifecycle: string; - // (undocumented) - namespace: string; - // (undocumented) - owner: string; -} +// @public @deprecated (undocumented) +export type CatalogEntityDocument = CatalogEntityDocument_2; // @public (undocumented) export type CatalogEnvironment = { - logger: Logger_2; + logger: Logger; database: PluginDatabaseManager; config: Config; reader: UrlReader; @@ -300,11 +270,6 @@ export type CatalogProcessor = { emit: CatalogProcessorEmit, cache: CatalogProcessorCache, ): Promise; - handleError?( - error: Error, - location: LocationSpec, - emit: CatalogProcessorEmit, - ): Promise; }; // @public @@ -334,7 +299,6 @@ export type CatalogProcessorErrorResult = { export type CatalogProcessorLocationResult = { type: 'location'; location: LocationSpec; - optional?: boolean; }; // @public @@ -347,7 +311,6 @@ export type CatalogProcessorParser = (options: { export type CatalogProcessorRelationResult = { type: 'relation'; relation: EntityRelationSpec; - entityRef?: string; }; // @public (undocumented) @@ -377,14 +340,14 @@ export type CatalogRulesEnforcer = { export class CodeOwnersProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - logger: Logger_2; + logger: Logger; reader: UrlReader; }); // (undocumented) static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; reader: UrlReader; }, ): CodeOwnersProcessor; @@ -394,17 +357,23 @@ export class CodeOwnersProcessor implements CatalogProcessor { preProcessEntity(entity: Entity, location: LocationSpec): Promise; } -// @public +// @alpha export const createCatalogPermissionRule: ( rule: PermissionRule, ) => PermissionRule; -// @public +// @alpha export const createCatalogPolicyDecision: ( conditions: PermissionCriteria>, ) => ConditionalPolicyDecision; // @public +export function createRandomProcessingInterval(options: { + minSeconds: number; + maxSeconds: number; +}): ProcessingIntervalFunction; + +// @public @deprecated export function createRandomRefreshInterval(options: { minSeconds: number; maxSeconds: number; @@ -413,8 +382,8 @@ export function createRandomRefreshInterval(options: { // @public export function createRouter(options: RouterOptions): Promise; -// @public (undocumented) -export class DefaultCatalogCollator implements DocumentCollator { +// @public @deprecated (undocumented) +export class DefaultCatalogCollator { constructor(options: { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; @@ -432,7 +401,7 @@ export class DefaultCatalogCollator implements DocumentCollator { // (undocumented) protected discovery: PluginEndpointDiscovery; // (undocumented) - execute(): Promise; + execute(): Promise; // (undocumented) protected filter?: GetEntitiesRequest['filter']; // (undocumented) @@ -454,6 +423,31 @@ export class DefaultCatalogCollator implements DocumentCollator { readonly visibilityPermission: Permission; } +// @public (undocumented) +export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { + // (undocumented) + static fromConfig( + _config: Config, + options: DefaultCatalogCollatorFactoryOptions, + ): DefaultCatalogCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type: string; + // (undocumented) + readonly visibilityPermission: Permission; +} + +// @public (undocumented) +export type DefaultCatalogCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + locationTemplate?: string; + filter?: GetEntitiesRequest['filter']; + batchSize?: number; + catalogClient?: CatalogApi; +}; + // @public (undocumented) export class DefaultCatalogProcessingOrchestrator implements CatalogProcessingOrchestrator @@ -461,7 +455,7 @@ export class DefaultCatalogProcessingOrchestrator constructor(options: { processors: CatalogProcessor[]; integrations: ScmIntegrationRegistry; - logger: Logger_2; + logger: Logger; parser: CatalogProcessorParser; policy: EntityPolicy; rulesEnforcer: CatalogRulesEnforcer; @@ -522,7 +516,7 @@ export type EntitiesSearchFilter = { values?: string[]; }; -// @public (undocumented) +// @public @deprecated (undocumented) function entity( atLocation: LocationSpec, newEntity: Entity, @@ -621,9 +615,9 @@ export type EntityProviderMutation = // @public export type EntityRelationSpec = { - source: EntityName; + source: CompoundEntityRef; type: string; - target: EntityName; + target: CompoundEntityRef; }; // @public (undocumented) @@ -639,7 +633,7 @@ export class FileReaderProcessor implements CatalogProcessor { ): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) function generalError( atLocation: LocationSpec, message: string, @@ -649,14 +643,14 @@ function generalError( export class GithubDiscoveryProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }); // (undocumented) static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }, ): GithubDiscoveryProcessor; @@ -677,11 +671,11 @@ export type GithubMultiOrgConfig = Array<{ userNamespace: string | undefined; }>; -// @alpha +// @public export class GithubMultiOrgReaderProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - logger: Logger_2; + logger: Logger; orgs: GithubMultiOrgConfig; githubCredentialsProvider?: GithubCredentialsProvider; }); @@ -689,7 +683,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }, ): GithubMultiOrgReaderProcessor; @@ -709,7 +703,7 @@ export class GitHubOrgEntityProvider implements EntityProvider { id: string; orgUrl: string; gitHubConfig: GitHubIntegrationConfig; - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }); // (undocumented) @@ -720,7 +714,7 @@ export class GitHubOrgEntityProvider implements EntityProvider { options: { id: string; orgUrl: string; - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }, ): GitHubOrgEntityProvider; @@ -734,14 +728,14 @@ export class GitHubOrgEntityProvider implements EntityProvider { export class GithubOrgReaderProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }); // (undocumented) static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }, ): GithubOrgReaderProcessor; @@ -755,35 +749,16 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { ): Promise; } -// @public -export class GitLabDiscoveryProcessor implements CatalogProcessor { - // (undocumented) - static fromConfig( - config: Config, - options: { - logger: Logger_2; - }, - ): GitLabDiscoveryProcessor; - // (undocumented) - getProcessorName(): string; - // (undocumented) - readLocation( - location: LocationSpec, - _optional: boolean, - emit: CatalogProcessorEmit, - ): Promise; -} - -// @public (undocumented) +// @public @deprecated (undocumented) function inputError( atLocation: LocationSpec, message: string, ): CatalogProcessorResult; -// @public (undocumented) +// @public @deprecated (undocumented) function location_2( newLocation: LocationSpec, - optional?: boolean, + _optional?: boolean, ): CatalogProcessorResult; // @public (undocumented) @@ -874,7 +849,7 @@ export interface LocationStore { listLocations(): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) function notFoundError( atLocation: LocationSpec, message: string, @@ -896,7 +871,7 @@ export function parseEntityYaml( location: LocationSpec, ): Iterable; -// @public +// @alpha export const permissionRules: { hasAnnotation: PermissionRule< Entity, @@ -961,6 +936,31 @@ export type PlaceholderResolverResolveUrl = ( base: string, ) => string; +// @public +export type ProcessingIntervalFunction = () => number; + +// @public +export const processingResult: Readonly<{ + readonly notFoundError: ( + atLocation: LocationSpec, + message: string, + ) => CatalogProcessorResult; + readonly inputError: ( + atLocation: LocationSpec, + message: string, + ) => CatalogProcessorResult; + readonly generalError: ( + atLocation: LocationSpec, + message: string, + ) => CatalogProcessorResult; + readonly location: (newLocation: LocationSpec) => CatalogProcessorResult; + readonly entity: ( + atLocation: LocationSpec, + newEntity: Entity, + ) => CatalogProcessorResult; + readonly relation: (spec: EntityRelationSpec) => CatalogProcessorResult; +}>; + // @public export type RecursivePartial = { [P in keyof T]?: T[P] extends (infer U)[] @@ -970,7 +970,7 @@ export type RecursivePartial = { : T[P]; }; -// @public +// @public @deprecated export type RefreshIntervalFunction = () => number; // @public @@ -984,7 +984,7 @@ export interface RefreshService { refresh(options: RefreshOptions): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) function relation(spec: EntityRelationSpec): CatalogProcessorResult; declare namespace results { @@ -1010,32 +1010,16 @@ export interface RouterOptions { // (undocumented) locationService: LocationService; // (undocumented) - logger: Logger_2; + logger: Logger; // (undocumented) permissionIntegrationRouter?: express.Router; // (undocumented) refreshService?: RefreshService; } -// @public @deprecated -export function runPeriodically(fn: () => any, delayMs: number): () => void; - -// @public @deprecated (undocumented) -export class StaticLocationProcessor implements StaticLocationProcessor { - constructor(staticLocations: LocationSpec[]); - // (undocumented) - static fromConfig(config: Config): StaticLocationProcessor; - // (undocumented) - readLocation( - location: LocationSpec, - _optional: boolean, - emit: CatalogProcessorEmit, - ): Promise; -} - // @public (undocumented) export class UrlReaderProcessor implements CatalogProcessor { - constructor(options: { reader: UrlReader; logger: Logger_2 }); + constructor(options: { reader: UrlReader; logger: Logger }); // (undocumented) getProcessorName(): string; // (undocumented) diff --git a/plugins/catalog-backend/migrations/20200702153613_entities.js b/plugins/catalog-backend/migrations/20200702153613_entities.js index fef8a7c074..6292063dac 100644 --- a/plugins/catalog-backend/migrations/20200702153613_entities.js +++ b/plugins/catalog-backend/migrations/20200702153613_entities.js @@ -21,7 +21,7 @@ */ exports.up = async function up(knex) { // SQLite does not support FK and PK - if (knex.client.config.client !== 'sqlite3') { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('entities_search', table => { table.dropForeign(['entity_id']); }); @@ -130,7 +130,7 @@ exports.up = async function up(knex) { */ exports.down = async function down(knex) { // SQLite does not support FK and PK - if (knex.client.config.client !== 'sqlite3') { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('entities_search', table => { table.dropForeign(['entity_id']); }); diff --git a/plugins/catalog-backend/migrations/20200807120600_entitySearch.js b/plugins/catalog-backend/migrations/20200807120600_entitySearch.js index aa05e79be9..9c7d966087 100644 --- a/plugins/catalog-backend/migrations/20200807120600_entitySearch.js +++ b/plugins/catalog-backend/migrations/20200807120600_entitySearch.js @@ -21,7 +21,7 @@ */ exports.up = async function up(knex) { // Sqlite does not support alter column. - if (knex.client.config.client !== 'sqlite3') { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('entities_search', table => { table.text('value').nullable().alter({ alterType: true }); }); @@ -33,7 +33,7 @@ exports.up = async function up(knex) { */ exports.down = async function down(knex) { // Sqlite does not support alter column. - if (knex.client.config.client !== 'sqlite3') { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('entities_search', table => { table.string('value').nullable().alter({ alterType: true }); }); diff --git a/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js b/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js index 366a4b7044..cd13f610cd 100644 --- a/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js +++ b/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js @@ -31,7 +31,7 @@ exports.up = async function up(knex) { }); // SQLite does not support alter column - if (knex.client.config.client !== 'sqlite3') { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('entities', table => { table.text('full_name').notNullable().alter({ alterNullable: true }); }); diff --git a/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js b/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js index 35b0474f06..47097fc788 100644 --- a/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js +++ b/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js @@ -41,7 +41,7 @@ exports.up = async function up(knex) { }); // SQLite does not support ALTER COLUMN. - if (knex.client.config.client !== 'sqlite3') { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('entities', table => { table.text('data').notNullable().alter({ alterNullable: true }); }); diff --git a/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js b/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js index b3c3a042f5..996c69de8c 100644 --- a/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js +++ b/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js @@ -20,7 +20,7 @@ * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { - if (knex.client.config.client === 'sqlite3') { + if (knex.client.config.client.includes('sqlite3')) { // sqlite doesn't support dropPrimary so we recreate it properly instead await knex.schema.dropTable('entities_relations'); await knex.schema.createTable('entities_relations', table => { @@ -58,7 +58,7 @@ exports.up = async function up(knex) { * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { - if (knex.client.config.client === 'sqlite3') { + if (knex.client.config.client.includes('sqlite3')) { await knex.schema.dropTable('entities_relations'); await knex.schema.createTable('entities_relations', table => { table.comment('All relations between entities in the catalog'); diff --git a/plugins/catalog-backend/migrations/20201210185851_fk_index.js b/plugins/catalog-backend/migrations/20201210185851_fk_index.js index 11907b24f4..84ec03f4e3 100644 --- a/plugins/catalog-backend/migrations/20201210185851_fk_index.js +++ b/plugins/catalog-backend/migrations/20201210185851_fk_index.js @@ -20,7 +20,7 @@ * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { - if (knex.client.config.client !== 'sqlite3') { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('entities_relations', table => { table.index('originating_entity_id', 'originating_entity_id_idx'); }); @@ -34,7 +34,7 @@ exports.up = async function up(knex) { * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { - if (knex.client.config.client !== 'sqlite3') { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('entities_relations', table => { table.dropIndex([], 'originating_entity_id_idx'); }); diff --git a/plugins/catalog-backend/migrations/20201230103504_update_log_varchar.js b/plugins/catalog-backend/migrations/20201230103504_update_log_varchar.js index 9a5ccce9fd..a12130e95f 100644 --- a/plugins/catalog-backend/migrations/20201230103504_update_log_varchar.js +++ b/plugins/catalog-backend/migrations/20201230103504_update_log_varchar.js @@ -20,7 +20,7 @@ * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { - if (knex.client.config.client !== 'sqlite3') { + if (!knex.client.config.client.includes('sqlite3')) { // We actually just want to widen columns, but can't do that while a // view is dependent on them - so we just reconstruct it exactly as it was await knex.schema @@ -49,7 +49,7 @@ exports.up = async function up(knex) { * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { - if (knex.client.config.client !== 'sqlite3') { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema .raw('DROP VIEW location_update_log_latest;') .alterTable('location_update_log', table => { diff --git a/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js b/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js index 80925f47ce..34f465de9a 100644 --- a/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js +++ b/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js @@ -20,7 +20,7 @@ * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { - if (knex.client.config.client !== 'sqlite3') { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('entities', table => { table.index('location_id', 'entity_location_id_idx'); }); @@ -34,7 +34,7 @@ exports.up = async function up(knex) { * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { - if (knex.client.config.client !== 'sqlite3') { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('entities', table => { table.dropIndex([], 'entity_location_id_idx'); }); diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 55c92c92be..700141fb94 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "0.22.0", + "version": "0.24.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -9,7 +9,8 @@ "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "backend-plugin" @@ -25,7 +26,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -33,27 +34,26 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/plugin-catalog-common": "^0.1.4", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-permission-node": "^0.5.2", - "@backstage/plugin-scaffolder-common": "^0.2.2", - "@backstage/search-common": "^0.2.4", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-catalog-common": "^0.2.2-next.0", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-permission-node": "^0.5.4-next.0", + "@backstage/plugin-scaffolder-common": "^0.3.0-next.0", + "@backstage/plugin-search-common": "^0.3.1-next.0", "@backstage/types": "^0.1.3", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", - "aws-sdk": "^2.840.0", "codeowners-utils": "^1.0.2", "core-js": "^3.6.5", "express": "^4.17.1", "express-promise-router": "^4.1.0", "fast-json-stable-stringify": "^2.1.0", - "fs-extra": "9.1.0", + "fs-extra": "10.0.1", "git-url-parse": "^11.6.0", "glob": "^7.1.6", "knex": "^1.0.2", @@ -69,17 +69,16 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.19", - "@backstage/cli": "^0.14.1", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/backend-test-utils": "^0.1.21-next.0", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-search-backend-node": "0.5.1-next.0", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", "@vscode/sqlite3": "^5.0.7", - "aws-sdk-mock": "^5.2.1", "msw": "^0.35.0", "supertest": "^6.1.3", "wait-for-expect": "^3.0.2", @@ -87,6 +86,7 @@ }, "files": [ "dist", + "alpha", "migrations/**/*.{js,d.ts}", "config.d.ts" ], diff --git a/packages/catalog-model/src/location/types.ts b/plugins/catalog-backend/src/api/common.ts similarity index 71% rename from packages/catalog-model/src/location/types.ts rename to plugins/catalog-backend/src/api/common.ts index 9b87e4919d..f3b4a387ba 100644 --- a/packages/catalog-model/src/location/types.ts +++ b/plugins/catalog-backend/src/api/common.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2022 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,8 @@ * limitations under the License. */ +import { CompoundEntityRef } from '@backstage/catalog-model'; + /** * Holds the entity location information. * @@ -24,7 +26,6 @@ * default value: 'required'. * * @public - * @deprecated Import from `@backstage/plugin-catalog-backend` instead. */ export type LocationSpec = { type: string; @@ -33,11 +34,23 @@ export type LocationSpec = { }; /** - * Entity location for a specific entity. + * Holds the relation data for entities. * * @public - * @deprecated import from {@link @backstage/catalog-client#Location} instead. */ -export type Location = { - id: string; -} & LocationSpec; +export type EntityRelationSpec = { + /** + * The source entity of this relation. + */ + source: CompoundEntityRef; + + /** + * The type of the relation. + */ + type: string; + + /** + * The target entity of this relation. + */ + target: CompoundEntityRef; +}; diff --git a/plugins/catalog-backend/src/ingestion/processors/results.ts b/plugins/catalog-backend/src/api/deprecatedResult.ts similarity index 66% rename from plugins/catalog-backend/src/ingestion/processors/results.ts rename to plugins/catalog-backend/src/api/deprecatedResult.ts index 01583678df..e616f3cf7d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/results.ts +++ b/plugins/catalog-backend/src/api/deprecatedResult.ts @@ -16,10 +16,15 @@ import { InputError, NotFoundError } from '@backstage/errors'; import { Entity } from '@backstage/catalog-model'; -import { CatalogProcessorResult, LocationSpec } from './types'; -import { EntityRelationSpec } from '../../processing/types'; +import { CatalogProcessorResult } from './processor'; +import { EntityRelationSpec, LocationSpec } from './common'; -/** @public */ +// NOTE: This entire file is deprecated and should be eventually removed along with the `result` export + +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function notFoundError( atLocation: LocationSpec, message: string, @@ -31,7 +36,10 @@ export function notFoundError( }; } -/** @public */ +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function inputError( atLocation: LocationSpec, message: string, @@ -43,7 +51,10 @@ export function inputError( }; } -/** @public */ +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function generalError( atLocation: LocationSpec, message: string, @@ -51,15 +62,21 @@ export function generalError( return { type: 'error', location: atLocation, error: new Error(message) }; } -/** @public */ +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function location( newLocation: LocationSpec, - optional?: boolean, + _optional?: boolean, ): CatalogProcessorResult { - return { type: 'location', location: newLocation, optional }; + return { type: 'location', location: newLocation }; } -/** @public */ +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function entity( atLocation: LocationSpec, newEntity: Entity, @@ -67,7 +84,10 @@ export function entity( return { type: 'entity', location: atLocation, entity: newEntity }; } -/** @public */ +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function relation(spec: EntityRelationSpec): CatalogProcessorResult { return { type: 'relation', relation: spec }; } diff --git a/plugins/catalog-backend/src/api/index.ts b/plugins/catalog-backend/src/api/index.ts new file mode 100644 index 0000000000..3f1c627743 --- /dev/null +++ b/plugins/catalog-backend/src/api/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2022 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 * as results from './deprecatedResult'; + +export { results }; + +export { processingResult } from './processingResult'; +export type { EntityRelationSpec, LocationSpec } from './common'; +export type { + CatalogProcessor, + CatalogProcessorParser, + CatalogProcessorCache, + CatalogProcessorEmit, + CatalogProcessorLocationResult, + CatalogProcessorEntityResult, + CatalogProcessorRelationResult, + CatalogProcessorErrorResult, + CatalogProcessorResult, +} from './processor'; +export type { + EntityProvider, + EntityProviderConnection, + EntityProviderMutation, +} from './provider'; diff --git a/plugins/catalog-backend/src/api/processingResult.ts b/plugins/catalog-backend/src/api/processingResult.ts new file mode 100644 index 0000000000..2fc2c9eac7 --- /dev/null +++ b/plugins/catalog-backend/src/api/processingResult.ts @@ -0,0 +1,68 @@ +/* + * 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 { InputError, NotFoundError } from '@backstage/errors'; +import { Entity } from '@backstage/catalog-model'; +import { CatalogProcessorResult } from './processor'; +import { EntityRelationSpec, LocationSpec } from './common'; + +/** + * Factory functions for the standard processing result types. + * + * @public + */ +export const processingResult = Object.freeze({ + notFoundError( + atLocation: LocationSpec, + message: string, + ): CatalogProcessorResult { + return { + type: 'error', + location: atLocation, + error: new NotFoundError(message), + }; + }, + + inputError( + atLocation: LocationSpec, + message: string, + ): CatalogProcessorResult { + return { + type: 'error', + location: atLocation, + error: new InputError(message), + }; + }, + + generalError( + atLocation: LocationSpec, + message: string, + ): CatalogProcessorResult { + return { type: 'error', location: atLocation, error: new Error(message) }; + }, + + location(newLocation: LocationSpec): CatalogProcessorResult { + return { type: 'location', location: newLocation }; + }, + + entity(atLocation: LocationSpec, newEntity: Entity): CatalogProcessorResult { + return { type: 'entity', location: atLocation, entity: newEntity }; + }, + + relation(spec: EntityRelationSpec): CatalogProcessorResult { + return { type: 'relation', relation: spec }; + }, +} as const); diff --git a/plugins/catalog-backend/src/ingestion/processors/types.ts b/plugins/catalog-backend/src/api/processor.ts similarity index 86% rename from plugins/catalog-backend/src/ingestion/processors/types.ts rename to plugins/catalog-backend/src/api/processor.ts index 879b4d32a8..3f274d13ea 100644 --- a/plugins/catalog-backend/src/ingestion/processors/types.ts +++ b/plugins/catalog-backend/src/api/processor.ts @@ -16,24 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; -import { EntityRelationSpec } from '../../processing/types'; - -/** - * Holds the entity location information. - * - * @remarks - * - * `presence` flag: when using repo importer plugin, location is being created before the component yaml file is merged to the main branch. - * This flag is then set to indicate that the file can be not present. - * default value: 'required'. - * - * @public - */ -export type LocationSpec = { - type: string; - target: string; - presence?: 'optional' | 'required'; -}; +import { EntityRelationSpec, LocationSpec } from './common'; /** * @public @@ -116,20 +99,6 @@ export type CatalogProcessor = { emit: CatalogProcessorEmit, cache: CatalogProcessorCache, ): Promise; - - /** - * Handles an emitted error. - * - * @param error - The error - * @param location - The location where the error occurred - * @param emit - A sink for items resulting from this handling - * @returns Nothing - */ - handleError?( - error: Error, - location: LocationSpec, - emit: CatalogProcessorEmit, - ): Promise; }; /** @@ -178,8 +147,6 @@ export type CatalogProcessorEmit = (generated: CatalogProcessorResult) => void; export type CatalogProcessorLocationResult = { type: 'location'; location: LocationSpec; - /** @deprecated Set `location.presence = 'optional'` instead */ - optional?: boolean; }; /** @public */ @@ -193,7 +160,6 @@ export type CatalogProcessorEntityResult = { export type CatalogProcessorRelationResult = { type: 'relation'; relation: EntityRelationSpec; - entityRef?: string; }; /** @public */ diff --git a/plugins/catalog-backend/src/providers/types.ts b/plugins/catalog-backend/src/api/provider.ts similarity index 96% rename from plugins/catalog-backend/src/providers/types.ts rename to plugins/catalog-backend/src/api/provider.ts index 226c021e3c..fa7659ee32 100644 --- a/plugins/catalog-backend/src/providers/types.ts +++ b/plugins/catalog-backend/src/api/provider.ts @@ -44,7 +44,7 @@ export interface EntityProviderConnection { * @public */ export interface EntityProvider { - /** Unique name provider name used internally for caching. */ + /** Unique provider name used internally for caching. */ getProviderName(): string; /** Connect is called upon initialization by the catalog engine. */ connect(connection: EntityProviderConnection): Promise; diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts index 729be442aa..c7cd6b7026 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts @@ -28,7 +28,7 @@ import { DbRefreshStateRow, DbRelationsRow, } from './tables'; -import { createRandomRefreshInterval } from '../processing/refresh'; +import { createRandomProcessingInterval } from '../processing/refresh'; import { timestampToDateTime } from './conversion'; import { generateStableHash } from './util'; @@ -49,7 +49,7 @@ describe('Default Processing Database', () => { db: new DefaultProcessingDatabase({ database: knex, logger, - refreshInterval: createRandomRefreshInterval({ + refreshInterval: createRandomProcessingInterval({ minSeconds: 100, maxSeconds: 150, }), diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts index 55f04c289c..6ffd054435 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts @@ -403,10 +403,9 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { items.map(i => i.entity_ref), ) .update({ - next_update_at: - tx.client.config.client === 'sqlite3' - ? tx.raw(`datetime('now', ?)`, [`${interval} seconds`]) - : tx.raw(`now() + interval '${interval} seconds'`), + next_update_at: tx.client.config.client.includes('sqlite3') + ? tx.raw(`datetime('now', ?)`, [`${interval} seconds`]) + : tx.raw(`now() + interval '${interval} seconds'`), }); return { @@ -589,7 +588,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { // We have to do this because the only way to detect if there was a conflict with // SQLite is to catch the error, while Postgres needs to ignore the conflict to not // break the ongoing transaction. - if (tx.client.config.client !== 'sqlite3') { + if (!tx.client.config.client.includes('sqlite3')) { query = query.onConflict('entity_ref').ignore() as any; // type here does not match runtime } diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 08201323ea..1affb14b45 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -17,7 +17,8 @@ import { Entity } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; import { DateTime } from 'luxon'; -import { DeferredEntity, EntityRelationSpec } from '../processing/types'; +import { EntityRelationSpec } from '../api'; +import { DeferredEntity } from '../processing/types'; /** * An abstraction for transactions of the underlying database technology. diff --git a/plugins/catalog-backend/src/index.ts b/plugins/catalog-backend/src/index.ts index efdaa4b8c5..4c1e753300 100644 --- a/plugins/catalog-backend/src/index.ts +++ b/plugins/catalog-backend/src/index.ts @@ -20,11 +20,12 @@ * @packageDocumentation */ +export * from './api'; export * from './catalog'; export * from './ingestion'; +export * from './modules'; export * from './search'; export * from './util'; export * from './processing'; -export * from './providers'; export * from './service'; export * from './permissions'; diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts index d29a445a8c..13430f51dd 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts @@ -17,7 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { DefaultCatalogRulesEnforcer } from './CatalogRules'; -import { LocationSpec } from './processors'; +import { LocationSpec } from '../api'; const entity = { user: { diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index b4ad394168..e4bfdb8d0f 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -17,7 +17,7 @@ import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import path from 'path'; -import { LocationSpec } from './processors'; +import { LocationSpec } from '../api'; /** * Rules to apply to catalog entities. diff --git a/plugins/catalog-backend/src/ingestion/index.ts b/plugins/catalog-backend/src/ingestion/index.ts index fcb634914b..1f4729b041 100644 --- a/plugins/catalog-backend/src/ingestion/index.ts +++ b/plugins/catalog-backend/src/ingestion/index.ts @@ -16,8 +16,6 @@ export { DefaultCatalogRulesEnforcer } from './CatalogRules'; export type { CatalogRule, CatalogRulesEnforcer } from './CatalogRules'; -export * from './processors'; -export * from './providers'; export type { AnalyzeLocationEntityField, AnalyzeLocationExistingEntity, diff --git a/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts deleted file mode 100644 index 1c2c27f425..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts +++ /dev/null @@ -1,56 +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 { Config } from '@backstage/config'; -import * as result from './results'; -import { CatalogProcessorEmit, LocationSpec } from './types'; - -/** - * @deprecated no longer in use, replaced by the ConfigLocationEntityProvider. - * @public - */ -export class StaticLocationProcessor implements StaticLocationProcessor { - static fromConfig(config: Config): StaticLocationProcessor { - const locations: LocationSpec[] = []; - - const lConfigs = config.getOptionalConfigArray('catalog.locations') ?? []; - for (const lConfig of lConfigs) { - const type = lConfig.getString('type'); - const target = lConfig.getString('target'); - locations.push({ type, target }); - } - - return new StaticLocationProcessor(locations); - } - - constructor(private readonly staticLocations: LocationSpec[]) {} - - async readLocation( - location: LocationSpec, - _optional: boolean, - emit: CatalogProcessorEmit, - ): Promise { - if (location.type !== 'bootstrap') { - return false; - } - - for (const staticLocation of this.staticLocations) { - emit(result.location(staticLocation)); - } - - return true; - } -} diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index eccf583e4b..6d1e9a3927 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { RecursivePartial } from '../util/RecursivePartial'; -import { LocationSpec } from './processors'; +import { LocationSpec } from '../api'; /** @public */ export type LocationAnalyzer = { @@ -62,7 +62,7 @@ export type AnalyzeLocationExistingEntity = { * enough info for the frontend to know what form data to show to the user * for overriding/completing the info. * @public - * */ + */ export type AnalyzeLocationGenerateEntity = { // Some form of partial representation of the entity entity: RecursivePartial; diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.test.ts similarity index 71% rename from plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts rename to plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.test.ts index 9ee8bf1b57..a25a5da89e 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.test.ts @@ -13,19 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getVoidLogger } from '@backstage/backend-common'; import { BitbucketDiscoveryProcessor } from './BitbucketDiscoveryProcessor'; import { ConfigReader } from '@backstage/config'; -import { - BitbucketRepository20, - BitbucketRepositoryParser, - PagedResponse, - PagedResponse20, -} from './bitbucket'; -import { LocationSpec } from './types'; -import { results } from './index'; import { RequestHandler, rest } from 'msw'; import { setupServer } from 'msw/node'; +import { BitbucketRepository20, PagedResponse, PagedResponse20 } from './lib'; +import { LocationSpec, processingResult } from '../../api'; const server = setupServer(); @@ -121,6 +116,60 @@ function setupBitbucketCloudStubs( return stubCallerFn; } +function setupBitbucketCloudSearchStubs( + workspace: string, + repositories: Pick[], + catalogPath: string, +) { + const stubCallerFn = jest.fn(); + function pagedResponse(values: any): PagedResponse20 { + return { + values: values, + page: 1, + } as PagedResponse20; + } + + server.use( + rest.get( + `https://api.bitbucket.org/2.0/workspaces/${workspace}/search/code`, + (req, res, ctx) => { + stubCallerFn(req); + return res( + ctx.json( + pagedResponse( + repositories.map(r => ({ + type: 'code_search_result', + content_match_count: 0, + content_matches: [], + path_matches: [ + catalogPath + .split('/') + .flatMap(seg => [{ text: '/' }, { text: seg, match: true }]) + .slice(1), + ], + file: { + commit: { + repository: { + ...r, + links: { + html: { + href: `https://bitbucket.org/${workspace}/${r.slug}`, + }, + }, + }, + }, + path: catalogPath, + }, + })), + ), + ), + ); + }, + ), + ); + return stubCallerFn; +} + describe('BitbucketDiscoveryProcessor', () => { beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); @@ -741,16 +790,257 @@ describe('BitbucketDiscoveryProcessor', () => { }); }); - describe('Custom repository parser', () => { - const customRepositoryParser: BitbucketRepositoryParser = - async function* customRepositoryParser({}) { - yield results.location({ - type: 'custom-location-type', - target: 'custom-target', - presence: 'optional', - }); + describe('handles cloud repositories using code search', () => { + const processor = BitbucketDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + username: 'myuser', + appPassword: 'blob', + }, + ], + }, + }), + { logger: getVoidLogger() }, + ); + + it('output all repositories by default', async () => { + setupBitbucketCloudSearchStubs( + 'myworkspace', + [ + { project: { key: 'prj-one' }, slug: 'repository-one' }, + { project: { key: 'prj-two' }, slug: 'repository-two' }, + ], + 'catalog-info.yaml', + ); + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: 'https://bitbucket.org/workspaces/myworkspace?search=true', }; + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toBeCalledTimes(2); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog-info.yaml', + presence: 'required', + }, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-two/src/master/catalog-info.yaml', + presence: 'required', + }, + }); + }); + + it('uses provided catalog path', async () => { + setupBitbucketCloudSearchStubs( + 'myworkspace', + [ + { project: { key: 'prj-one' }, slug: 'repository-one' }, + { project: { key: 'prj-two' }, slug: 'repository-two' }, + ], + 'my/nested/path/catalog.yaml', + ); + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: + 'https://bitbucket.org/workspaces/myworkspace?search=true&catalogPath=my/nested/path/catalog.yaml', + }; + + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toBeCalledTimes(2); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-one/src/master/my/nested/path/catalog.yaml', + presence: 'required', + }, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-two/src/master/my/nested/path/catalog.yaml', + presence: 'required', + }, + }); + }); + + it('output all repositories', async () => { + setupBitbucketCloudSearchStubs( + 'myworkspace', + [ + { project: { key: 'prj-one' }, slug: 'repository-one' }, + { project: { key: 'prj-two' }, slug: 'repository-two' }, + ], + 'catalog.yaml', + ); + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: + 'https://bitbucket.org/workspaces/myworkspace/projects/*/repos/*?search=true&catalogPath=catalog.yaml', + }; + + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toBeCalledTimes(2); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog.yaml', + presence: 'required', + }, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-two/src/master/catalog.yaml', + presence: 'required', + }, + }); + }); + + it('output repositories with wildcards', async () => { + setupBitbucketCloudSearchStubs( + 'myworkspace', + [ + { project: { key: 'prj-one' }, slug: 'repository-one' }, + { project: { key: 'prj-two' }, slug: 'repository-two' }, + ], + 'catalog.yaml', + ); + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: + 'https://bitbucket.org/workspaces/myworkspace/projects/prj-one/repos/*?search=true&catalogPath=catalog.yaml', + }; + + const emitter = jest.fn(); + await processor.readLocation(location, false, emitter); + + expect(emitter).toBeCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog.yaml', + presence: 'required', + }, + }); + }); + + it('filter unrelated repositories', async () => { + setupBitbucketCloudSearchStubs( + 'myworkspace', + [ + { project: { key: 'prj-one' }, slug: 'repository-one' }, + { project: { key: 'prj-one' }, slug: 'repository-two' }, + { project: { key: 'prj-one' }, slug: 'repository-three' }, + ], + 'catalog.yaml', + ); + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: + 'https://bitbucket.org/workspaces/myworkspace/projects/prj-one/repos/repository-three?search=true&catalogPath=catalog.yaml', + }; + + const emitter = jest.fn(); + await processor.readLocation(location, false, emitter); + + expect(emitter).toBeCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-three/src/master/catalog.yaml', + presence: 'required', + }, + }); + }); + + it.each` + target + ${'https://bitbucket.org/workspaces/myworkspace/projects/prj-one/repos/*?search=true'} + ${'https://bitbucket.org/workspaces/myworkspace/projects/prj-one/repos/*/?search=true'} + ${'https://bitbucket.org/workspaces/myworkspace/projects/prj-one/repos/repository-*/?search=true'} + `("target '$target' adds default path to catalog", async ({ target }) => { + setupBitbucketCloudSearchStubs( + 'myworkspace', + [{ project: { key: 'prj-one' }, slug: 'repository-one' }], + 'catalog-info.yaml', + ); + + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: target, + }; + + const emitter = jest.fn(); + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog-info.yaml', + presence: 'required', + }, + }); + }); + + it.each` + target + ${'https://bitbucket.org/test?search=true'} + `("target '$target' is rejected", async ({ target }) => { + setupBitbucketCloudSearchStubs( + 'myworkspace', + [{ project: { key: 'prj-one' }, slug: 'repository-one' }], + 'catalog-info.yaml', + ); + + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: target, + }; + + const emitter = jest.fn(); + await expect( + processor.readLocation(location, false, emitter), + ).rejects.toThrow(/Failed to parse /); + }); + }); + + describe('Custom repository parser', () => { const processor = BitbucketDiscoveryProcessor.fromConfig( new ConfigReader({ integrations: { @@ -763,7 +1053,16 @@ describe('BitbucketDiscoveryProcessor', () => { ], }, }), - { parser: customRepositoryParser, logger: getVoidLogger() }, + { + parser: async function* customRepositoryParser({}) { + yield processingResult.location({ + type: 'custom-location-type', + target: 'custom-target', + presence: 'optional', + }); + }, + logger: getVoidLogger(), + }, ); it('use custom repository parser', async () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts b/plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.ts similarity index 65% rename from plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts rename to plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.ts index 8fcf64dc55..49fa05a762 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Logger } from 'winston'; import { Config } from '@backstage/config'; @@ -22,29 +23,45 @@ import { ScmIntegrations, } from '@backstage/integration'; import { - BitbucketRepositoryParser, BitbucketClient, defaultRepositoryParser, paginated, paginated20, BitbucketRepository, BitbucketRepository20, -} from './bitbucket'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; +} from './lib'; +import { + CatalogProcessor, + CatalogProcessorEmit, + CatalogProcessorResult, + LocationSpec, +} from '../../api'; const DEFAULT_BRANCH = 'master'; const DEFAULT_CATALOG_LOCATION = '/catalog-info.yaml'; -const EMPTY_CATALOG_LOCATION = '/'; /** @public */ export class BitbucketDiscoveryProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; - private readonly parser: BitbucketRepositoryParser; + private readonly parser: (options: { + integration: BitbucketIntegration; + target: string; + presence?: 'optional' | 'required'; + logger: Logger; + }) => AsyncIterable; private readonly logger: Logger; static fromConfig( config: Config, - options: { parser?: BitbucketRepositoryParser; logger: Logger }, + options: { + parser?: (options: { + integration: BitbucketIntegration; + target: string; + presence?: 'optional' | 'required'; + logger: Logger; + }) => AsyncIterable; + logger: Logger; + }, ) { const integrations = ScmIntegrations.fromConfig(config); @@ -56,7 +73,12 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - parser?: BitbucketRepositoryParser; + parser?: (options: { + integration: BitbucketIntegration; + target: string; + presence?: 'optional' | 'required'; + logger: Logger; + }) => AsyncIterable; logger: Logger; }) { this.integrations = options.integrations; @@ -116,19 +138,17 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { ): Promise { const { client, location, integration, emit } = options; - const { catalogPath: requestedCatalogPath } = parseBitbucketCloudUrl( - location.target, - ); - const catalogPath = - requestedCatalogPath === EMPTY_CATALOG_LOCATION - ? DEFAULT_CATALOG_LOCATION - : requestedCatalogPath; - const result = await readBitbucketCloud(client, location.target); - for (const repository of result.matches) { - const mainbranch = repository.mainbranch?.name ?? DEFAULT_BRANCH; + const { searchEnabled } = parseBitbucketCloudUrl(location.target); + + const result = searchEnabled + ? await searchBitbucketCloudLocations(client, location.target) + : await readBitbucketCloudLocations(client, location.target); + + for (const locationTarget of result.matches) { for await (const entity of this.parser({ integration, - target: `${repository.links.html.href}/src/${mainbranch}${catalogPath}`, + target: locationTarget, + presence: searchEnabled ? 'required' : 'optional', logger: this.logger, })) { emit(entity); @@ -145,10 +165,10 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { ): Promise { const { client, location, integration, emit } = options; const { catalogPath: requestedCatalogPath } = parseUrl(location.target); - const catalogPath = - requestedCatalogPath === EMPTY_CATALOG_LOCATION - ? DEFAULT_CATALOG_LOCATION - : requestedCatalogPath; + const catalogPath = requestedCatalogPath + ? `/${requestedCatalogPath}` + : DEFAULT_CATALOG_LOCATION; + const result = await readBitbucketOrg(client, location.target); for (const repository of result.matches) { for await (const entity of this.parser({ @@ -194,6 +214,81 @@ export async function readBitbucketOrg( return result; } +export async function searchBitbucketCloudLocations( + client: BitbucketClient, + target: string, +): Promise> { + const { + workspacePath, + catalogPath: requestedCatalogPath, + projectSearchPath, + repoSearchPath, + } = parseBitbucketCloudUrl(target); + + const result: Result = { + scanned: 0, + matches: [], + }; + + const catalogPath = requestedCatalogPath + ? requestedCatalogPath + : DEFAULT_CATALOG_LOCATION; + const catalogFilename = catalogPath.substring( + catalogPath.lastIndexOf('/') + 1, + ); + + const searchResults = paginated20(options => + client.searchCode( + workspacePath, + `"${catalogFilename}" path:${catalogPath}`, + options, + ), + ); + + for await (const searchResult of searchResults) { + // not a file match, but a code match + if (searchResult.path_matches.length === 0) { + continue; + } + + const repository = searchResult.file.commit.repository; + if (!matchesPostFilters(repository, projectSearchPath, repoSearchPath)) { + continue; + } + + const repoUrl = repository.links.html.href; + const branch = repository.mainbranch?.name ?? DEFAULT_BRANCH; + const filePath = searchResult.file.path; + const location = `${repoUrl}/src/${branch}/${filePath}`; + + result.matches.push(location); + } + + return result; +} + +export async function readBitbucketCloudLocations( + client: BitbucketClient, + target: string, +): Promise> { + const { catalogPath: requestedCatalogPath } = parseBitbucketCloudUrl(target); + const catalogPath = requestedCatalogPath + ? `/${requestedCatalogPath}` + : DEFAULT_CATALOG_LOCATION; + + return readBitbucketCloud(client, target).then(result => { + const matches = result.matches.map(repository => { + const branch = repository.mainbranch?.name ?? DEFAULT_BRANCH; + return `${repository.links.html.href}/src/${branch}${catalogPath}`; + }); + + return { + scanned: result.scanned, + matches, + }; + }); +} + export async function readBitbucketCloud( client: BitbucketClient, target: string, @@ -218,16 +313,24 @@ export async function readBitbucketCloud( for await (const repository of repositories) { result.scanned++; - if ( - (!projectSearchPath || projectSearchPath.test(repository.project.key)) && - (!repoSearchPath || repoSearchPath.test(repository.slug)) - ) { + if (matchesPostFilters(repository, projectSearchPath, repoSearchPath)) { result.matches.push(repository); } } return result; } +function matchesPostFilters( + repository: BitbucketRepository20, + projectSearchPath: RegExp | undefined, + repoSearchPath: RegExp | undefined, +): boolean { + return ( + (!projectSearchPath || projectSearchPath.test(repository.project.key)) && + (!repoSearchPath || repoSearchPath.test(repository.slug)) + ); +} + function parseUrl(urlString: string): { projectSearchPath: RegExp; repoSearchPath: RegExp; @@ -243,9 +346,7 @@ function parseUrl(urlString: string): { return { projectSearchPath: escapeRegExp(decodeURIComponent(path[1])), repoSearchPath: escapeRegExp(decodeURIComponent(path[3])), - catalogPath: `/${decodeURIComponent( - path.slice(4).join('/') + url.search, - )}`, + catalogPath: decodeURIComponent(path.slice(4).join('/') + url.search), }; } @@ -262,10 +363,11 @@ function readPathParameters(pathParts: string[]): Map { function parseBitbucketCloudUrl(urlString: string): { workspacePath: string; - catalogPath: string; + catalogPath?: string; projectSearchPath?: RegExp; repoSearchPath?: RegExp; queryParam?: string; + searchEnabled: boolean; } { const url = new URL(urlString); const pathMap = readPathParameters(url.pathname.substr(1).split('/')); @@ -283,8 +385,9 @@ function parseBitbucketCloudUrl(urlString: string): { repoSearchPath: pathMap.has('repos') ? escapeRegExp(pathMap.get('repos')!) : undefined, - catalogPath: `/${query.get('catalogPath') || ''}`, + catalogPath: query.get('catalogPath') || undefined, queryParam: query.get('q') || undefined, + searchEnabled: query.get('search')?.toLowerCase() === 'true', }; } diff --git a/plugins/catalog-backend/src/modules/bitbucket/index.ts b/plugins/catalog-backend/src/modules/bitbucket/index.ts new file mode 100644 index 0000000000..0e39083520 --- /dev/null +++ b/plugins/catalog-backend/src/modules/bitbucket/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 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. + */ + +export { BitbucketDiscoveryProcessor } from './BitbucketDiscoveryProcessor'; +export type { BitbucketRepositoryParser } from './lib'; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.test.ts similarity index 83% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.test.ts index 35e24ed4cb..9f2d8b2daa 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.test.ts @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { processingResult } from '../../../api'; import { defaultRepositoryParser } from './BitbucketRepositoryParser'; -import { results } from '../index'; -import { getVoidLogger } from '@backstage/backend-common'; -import { BitbucketIntegration } from '@backstage/integration'; describe('BitbucketRepositoryParser', () => { describe('defaultRepositoryParser', () => { @@ -25,16 +24,14 @@ describe('BitbucketRepositoryParser', () => { 'https://bitbucket.mycompany.com/projects/project-key/repos/repo-slug/browse'; const path = '/catalog-info.yaml'; const expected = [ - results.location({ + processingResult.location({ type: 'url', target: `${browseUrl}${path}`, presence: 'optional', }), ]; const actual = await defaultRepositoryParser({ - integration: {} as BitbucketIntegration, target: `${browseUrl}${path}`, - logger: getVoidLogger(), }); let i = 0; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.ts similarity index 72% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.ts index 9eaec221b5..8fdcb8f73a 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.ts @@ -13,26 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CatalogProcessorResult } from '../types'; -import { results } from '../index'; -import { Logger } from 'winston'; -import { BitbucketIntegration } from '@backstage/integration'; -/** @public */ +import { BitbucketIntegration } from '@backstage/integration'; +import { Logger } from 'winston'; +import { CatalogProcessorResult, processingResult } from '../../../api'; + +/** + * @public + * @deprecated type inlined. + */ export type BitbucketRepositoryParser = (options: { integration: BitbucketIntegration; target: string; + presence?: 'optional' | 'required'; logger: Logger; }) => AsyncIterable; -export const defaultRepositoryParser: BitbucketRepositoryParser = - async function* defaultRepositoryParser({ target }) { - yield results.location({ +export const defaultRepositoryParser = + async function* defaultRepositoryParser(options: { + target: string; + presence?: 'optional' | 'required'; + }) { + yield processingResult.location({ type: 'url', - target: target, + target: options.target, // Not all locations may actually exist, since the user defined them as a wildcard pattern. // Thus, we emit them as optional and let the downstream processor find them while not outputting // an error if it couldn't. - presence: 'optional', + presence: options.presence ?? 'optional', }); }; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/client.ts similarity index 77% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/client.ts index 0c132bb61b..0ef6558719 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/lib/client.ts @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import fetch from 'node-fetch'; +import fetch from 'node-fetch'; import { BitbucketIntegrationConfig, getBitbucketRequestOptions, @@ -28,6 +28,39 @@ export class BitbucketClient { this.config = options.config; } + async searchCode( + workspace: string, + query: string, + options?: ListOptions20, + ): Promise> { + // load all fields relevant for creating refs later, but not more + const fields = [ + // exclude code/content match details + '-values.content_matches', + // include/add relevant repository details + '+values.file.commit.repository.mainbranch.name', + '+values.file.commit.repository.project.key', + '+values.file.commit.repository.slug', + // remove irrelevant links + '-values.*.links', + '-values.*.*.links', + '-values.*.*.*.links', + // ...except the one we need + '+values.file.commit.repository.links.html.href', + ].join(','); + + return this.pagedRequest20( + `${this.config.apiBaseUrl}/workspaces/${encodeURIComponent( + workspace, + )}/search/code`, + { + ...options, + fields: fields, + search_query: query, + }, + ); + } + async listProjects(options?: ListOptions): Promise> { return this.pagedRequest(`${this.config.apiBaseUrl}/projects`, options); } @@ -105,6 +138,22 @@ export class BitbucketClient { } } +export type CodeSearchResultItem = { + type: string; + content_match_count: number; + path_matches: Array<{ + text: string; + match?: boolean; + }>; + file: { + path: string; + type: string; + commit: { + repository: BitbucketRepository20; + }; + }; +}; + export type ListOptions = { [key: string]: number | undefined; limit?: number | undefined; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/index.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/index.ts index 4b28c7f115..a819bb3c64 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/lib/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { BitbucketClient, paginated, paginated20 } from './client'; export { defaultRepositoryParser } from './BitbucketRepositoryParser'; export type { PagedResponse, PagedResponse20 } from './client'; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/types.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/types.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts rename to plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts index 8d6d1f8ff7..8cf250de2a 100644 --- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts @@ -17,7 +17,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { CodeOwnersProcessor } from './CodeOwnersProcessor'; -import { LocationSpec } from './types'; +import { LocationSpec } from '../../api'; const mockCodeOwnersText = () => ` * @acme/team-foo @acme/team-bar diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts similarity index 95% rename from plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts rename to plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts index 93e3c1618a..6f6aee7161 100644 --- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts @@ -22,11 +22,10 @@ import { ScmIntegrations, } from '@backstage/integration'; import { Logger } from 'winston'; -import { findCodeOwnerByTarget } from './codeowners'; -import { CatalogProcessor, LocationSpec } from './types'; +import { CatalogProcessor, LocationSpec } from '../../api'; +import { findCodeOwnerByTarget } from './lib'; const ALLOWED_KINDS = ['API', 'Component', 'Domain', 'Resource', 'System']; - const ALLOWED_LOCATION_TYPES = ['url']; /** @public */ diff --git a/plugins/catalog-backend/src/modules/codeowners/index.ts b/plugins/catalog-backend/src/modules/codeowners/index.ts new file mode 100644 index 0000000000..6b7569ff07 --- /dev/null +++ b/plugins/catalog-backend/src/modules/codeowners/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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. + */ + +export { CodeOwnersProcessor } from './CodeOwnersProcessor'; diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/index.ts b/plugins/catalog-backend/src/modules/codeowners/lib/index.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/index.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/index.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/read.test.ts b/plugins/catalog-backend/src/modules/codeowners/lib/read.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/read.test.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/read.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/read.ts b/plugins/catalog-backend/src/modules/codeowners/lib/read.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/read.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/read.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/resolve.test.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/resolve.test.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/resolve.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/resolve.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/scm.ts b/plugins/catalog-backend/src/modules/codeowners/lib/scm.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/scm.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/scm.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.test.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.test.ts index 5f5b20d1c2..3911f7f743 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.test.ts @@ -17,7 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; -import { LocationSpec } from './types'; +import { LocationSpec } from '../../api'; import { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor'; describe('AnnotateLocationEntityProcessor', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.ts similarity index 96% rename from plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts rename to plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.ts index b8c31ed5a2..4af0b8cf61 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.ts @@ -25,7 +25,11 @@ import { } from '@backstage/catalog-model'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { identity, merge, pickBy } from 'lodash'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, +} from '../../api'; /** @public */ export class AnnotateLocationEntityProcessor implements CatalogProcessor { diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.test.ts index 311dd13789..1e553a44f8 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.test.ts @@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { AnnotateScmSlugEntityProcessor } from './AnnotateScmSlugEntityProcessor'; -import { LocationSpec } from './types'; +import { LocationSpec } from '../../api'; describe('AnnotateScmSlugEntityProcessor', () => { describe('github', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts rename to plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.ts index 4d3b3aff6c..e44db4d44d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.ts @@ -21,7 +21,7 @@ import { } from '@backstage/integration'; import parseGitUrl from 'git-url-parse'; import { identity, merge, pickBy } from 'lodash'; -import { CatalogProcessor, LocationSpec } from './types'; +import { CatalogProcessor, LocationSpec } from '../../api'; const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug'; diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts similarity index 94% rename from plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts index 2532293acf..7ae4b6b5a0 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts @@ -23,7 +23,6 @@ import { SystemEntity, UserEntity, } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; import { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; describe('BuiltinKindsEntityProcessor', () => { @@ -539,38 +538,5 @@ describe('BuiltinKindsEntityProcessor', () => { }, }); }); - it('generates relations for template entities', async () => { - const entity: TemplateEntityV1beta2 = { - apiVersion: 'backstage.io/v1beta2', - kind: 'Template', - metadata: { name: 'n' }, - spec: { - parameters: {}, - steps: [], - type: 'service', - owner: 'o', - }, - }; - - await processor.postProcessEntity(entity, location, emit); - - expect(emit).toBeCalledTimes(2); - expect(emit).toBeCalledWith({ - type: 'relation', - relation: { - source: { kind: 'Group', namespace: 'default', name: 'o' }, - type: 'ownerOf', - target: { kind: 'Template', namespace: 'default', name: 'n' }, - }, - }); - expect(emit).toBeCalledWith({ - type: 'relation', - relation: { - source: { kind: 'Template', namespace: 'default', name: 'n' }, - type: 'ownedBy', - target: { kind: 'Group', namespace: 'default', name: 'o' }, - }, - }); - }); }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts similarity index 91% rename from plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts rename to plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts index 89b0bb1bb5..4e8cf3fef7 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts @@ -22,7 +22,7 @@ import { DomainEntity, domainEntityV1alpha1Validator, Entity, - getEntityName, + getCompoundEntityRef, GroupEntity, groupEntityV1alpha1Validator, locationEntityV1alpha1Validator, @@ -49,11 +49,11 @@ import { userEntityV1alpha1Validator, } from '@backstage/catalog-model'; import { - TemplateEntityV1beta2, - templateEntityV1beta2Validator, -} from '@backstage/plugin-scaffolder-common'; -import * as result from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; /** @public */ export class BuiltinKindsEntityProcessor implements CatalogProcessor { @@ -63,7 +63,6 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { resourceEntityV1alpha1Validator, groupEntityV1alpha1Validator, locationEntityV1alpha1Validator, - templateEntityV1beta2Validator, userEntityV1alpha1Validator, systemEntityV1alpha1Validator, domainEntityV1alpha1Validator, @@ -89,7 +88,7 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { _location: LocationSpec, emit: CatalogProcessorEmit, ): Promise { - const selfRef = getEntityName(entity); + const selfRef = getCompoundEntityRef(entity); /* * Utilities @@ -107,7 +106,7 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { for (const target of [targets].flat()) { const targetRef = parseEntityRef(target, context); emit( - result.relation({ + processingResult.relation({ source: selfRef, type: outgoingRelation, target: { @@ -118,7 +117,7 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { }), ); emit( - result.relation({ + processingResult.relation({ source: { kind: targetRef.kind, namespace: targetRef.namespace, @@ -131,19 +130,6 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { } } - /* - * Emit relations for the Template kind - */ - if (entity.kind === 'Template') { - const template = entity as TemplateEntityV1beta2; - doEmit( - template.spec.owner, - { defaultKind: 'Group', defaultNamespace: selfRef.namespace }, - RELATION_OWNED_BY, - RELATION_OWNER_OF, - ); - } - /* * Emit relations for the Component kind */ diff --git a/plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.test.ts b/plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.test.ts similarity index 98% rename from plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.test.ts rename to plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.test.ts index 0663ad8d04..d61342e3d0 100644 --- a/plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.test.ts +++ b/plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.test.ts @@ -17,7 +17,7 @@ import { ConfigReader } from '@backstage/config'; import path from 'path'; import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider'; -import { EntityProviderConnection } from './types'; +import { EntityProviderConnection } from '../../api'; describe('ConfigLocationEntityProvider', () => { it('should apply mutation with the correct paths in the config', async () => { diff --git a/plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.ts b/plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.ts similarity index 90% rename from plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.ts rename to plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.ts index 389cd8d9ff..9529a5b608 100644 --- a/plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.ts +++ b/plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.ts @@ -16,9 +16,9 @@ import { Config } from '@backstage/config'; import path from 'path'; -import { getEntityLocationRef } from '../processing/util'; -import { EntityProvider, EntityProviderConnection } from './types'; -import { locationSpecToLocationEntity } from '../util/conversion'; +import { getEntityLocationRef } from '../../processing/util'; +import { EntityProvider, EntityProviderConnection } from '../../api'; +import { locationSpecToLocationEntity } from '../../util/conversion'; export class ConfigLocationEntityProvider implements EntityProvider { constructor(private readonly config: Config) {} diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.test.ts similarity index 98% rename from plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts rename to plugins/catalog-backend/src/modules/core/DefaultLocationStore.test.ts index b9d129de5f..dfbc37f6c9 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts +++ b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.test.ts @@ -15,7 +15,7 @@ */ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { v4 as uuid } from 'uuid'; -import { applyDatabaseMigrations } from '../database/migrations'; +import { applyDatabaseMigrations } from '../../database/migrations'; import { DefaultLocationStore } from './DefaultLocationStore'; describe('DefaultLocationStore', () => { diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.ts similarity index 93% rename from plugins/catalog-backend/src/providers/DefaultLocationStore.ts rename to plugins/catalog-backend/src/modules/core/DefaultLocationStore.ts index 9d5971508f..d637f4eda0 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts +++ b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.ts @@ -18,11 +18,11 @@ import { Location } from '@backstage/catalog-client'; import { ConflictError, NotFoundError } from '@backstage/errors'; import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; -import { DbLocationsRow } from '../database/tables'; -import { getEntityLocationRef } from '../processing/util'; -import { EntityProvider, EntityProviderConnection } from './types'; -import { locationSpecToLocationEntity } from '../util/conversion'; -import { LocationInput, LocationStore } from '../service'; +import { DbLocationsRow } from '../../database/tables'; +import { getEntityLocationRef } from '../../processing/util'; +import { EntityProvider, EntityProviderConnection } from '../../api'; +import { locationSpecToLocationEntity } from '../../util/conversion'; +import { LocationInput, LocationStore } from '../../service'; export class DefaultLocationStore implements LocationStore, EntityProvider { private _connection: EntityProviderConnection | undefined; diff --git a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.test.ts similarity index 94% rename from plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/FileReaderProcessor.test.ts index c2cce66bc6..a3eb5e4542 100644 --- a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.test.ts @@ -19,12 +19,16 @@ import { CatalogProcessorEntityResult, CatalogProcessorErrorResult, CatalogProcessorResult, -} from './types'; +} from '../../api'; import path from 'path'; -import { defaultEntityDataParser } from './util/parse'; +import { defaultEntityDataParser } from '../util/parse'; describe('FileReaderProcessor', () => { - const fixturesRoot = path.join(__dirname, '__fixtures__/fileReaderProcessor'); + const fixturesRoot = path.join( + __dirname, + '__fixtures__', + 'fileReaderProcessor', + ); it('should load from file', async () => { const processor = new FileReaderProcessor(); diff --git a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts similarity index 92% rename from plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts rename to plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts index 996beff24a..66c6479fa7 100644 --- a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts @@ -18,13 +18,13 @@ import fs from 'fs-extra'; import g from 'glob'; import path from 'path'; import { promisify } from 'util'; -import * as result from './results'; import { CatalogProcessor, CatalogProcessorEmit, CatalogProcessorParser, LocationSpec, -} from './types'; + processingResult, +} from '../../api'; const glob = promisify(g); @@ -65,11 +65,11 @@ export class FileReaderProcessor implements CatalogProcessor { } } else if (!optional) { const message = `${location.type} ${location.target} does not exist`; - emit(result.notFoundError(location, message)); + emit(processingResult.notFoundError(location, message)); } } catch (e) { const message = `${location.type} ${location.target} could not be read, ${e}`; - emit(result.generalError(location, message)); + emit(processingResult.generalError(location, message)); } return true; diff --git a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/LocationEntityProcessor.test.ts index 83f1ca1925..92ba1fcebf 100644 --- a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/integration'; import path from 'path'; import { toAbsoluteUrl } from './LocationEntityProcessor'; -import { LocationSpec } from './types'; +import { LocationSpec } from '../../api'; describe('LocationEntityProcessor', () => { describe('toAbsoluteUrl', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.ts similarity index 92% rename from plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts rename to plugins/catalog-backend/src/modules/core/LocationEntityProcessor.ts index 533a366f57..c168f54bf1 100644 --- a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.ts @@ -17,8 +17,12 @@ import { Entity, LocationEntity } from '@backstage/catalog-model'; import { ScmIntegrationRegistry } from '@backstage/integration'; import path from 'path'; -import * as result from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; +import { + processingResult, + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, +} from '../../api'; export function toAbsoluteUrl( integrations: ScmIntegrationRegistry, @@ -62,7 +66,7 @@ export class LocationEntityProcessor implements CatalogProcessor { const type = locationEntity.spec.type || location.type; if (type === 'file' && location.target.endsWith(path.sep)) { emit( - result.inputError( + processingResult.inputError( location, `LocationEntityProcessor cannot handle ${type} type location with target ${location.target} that ends with a path separator`, ), @@ -83,7 +87,7 @@ export class LocationEntityProcessor implements CatalogProcessor { location, maybeRelativeTarget, ); - emit(result.location({ type, target })); + emit(processingResult.location({ type, target })); } } diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts rename to plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts index 0d572a024d..ddd5f951db 100644 --- a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts @@ -19,7 +19,7 @@ import { Entity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; import yaml from 'yaml'; -import { CatalogProcessor, LocationSpec } from './types'; +import { CatalogProcessor, LocationSpec } from '../../api'; /** @public */ export type PlaceholderResolverRead = (url: string) => Promise; diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts index b7dc0084f2..dbcb9e34df 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts @@ -19,8 +19,8 @@ import { UrlReader, UrlReaders, } from '@backstage/backend-common'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { @@ -28,9 +28,9 @@ import { CatalogProcessorEntityResult, CatalogProcessorErrorResult, CatalogProcessorResult, -} from './types'; +} from '../../api'; +import { defaultEntityDataParser } from '../util/parse'; import { UrlReaderProcessor } from './UrlReaderProcessor'; -import { defaultEntityDataParser } from './util/parse'; describe('UrlReaderProcessor', () => { const mockApiOrigin = 'http://localhost'; diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts similarity index 95% rename from plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts rename to plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts index c2be454bcf..7f27bcd19d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts @@ -20,7 +20,6 @@ import { assertError } from '@backstage/errors'; import parseGitUrl from 'git-url-parse'; import limiterFactory from 'p-limit'; import { Logger } from 'winston'; -import * as result from './results'; import { CatalogProcessor, CatalogProcessorCache, @@ -29,7 +28,8 @@ import { CatalogProcessorParser, CatalogProcessorResult, LocationSpec, -} from './types'; + processingResult, +} from '../../api'; const CACHE_KEY = 'v1'; @@ -102,10 +102,10 @@ export class UrlReaderProcessor implements CatalogProcessor { } } else if (error.name === 'NotFoundError') { if (!optional) { - emit(result.notFoundError(location, message)); + emit(processingResult.notFoundError(location, message)); } } else { - emit(result.generalError(location, message)); + emit(processingResult.generalError(location, message)); } } diff --git a/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt new file mode 100644 index 0000000000..7470c0e8a3 --- /dev/null +++ b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt @@ -0,0 +1 @@ +site_name: Test diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/component.yaml b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/component.yaml similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/component.yaml rename to plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/component.yaml diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/dir/api.yaml b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/dir/api.yaml similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/dir/api.yaml rename to plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/dir/api.yaml diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/test.txt b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/test.txt similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/test.txt rename to plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/test.txt diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/modules/core/index.ts similarity index 60% rename from plugins/catalog-backend/src/ingestion/processors/index.ts rename to plugins/catalog-backend/src/modules/core/index.ts index 84a92fb1ba..364cede069 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/modules/core/index.ts @@ -14,20 +14,10 @@ * limitations under the License. */ -import * as results from './results'; - export { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor'; export { AnnotateScmSlugEntityProcessor } from './AnnotateScmSlugEntityProcessor'; -export { AwsS3DiscoveryProcessor } from './AwsS3DiscoveryProcessor'; -export { BitbucketDiscoveryProcessor } from './BitbucketDiscoveryProcessor'; export { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; -export { CodeOwnersProcessor } from './CodeOwnersProcessor'; export { FileReaderProcessor } from './FileReaderProcessor'; -export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor'; -export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor'; -export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; -export { GithubMultiOrgReaderProcessor } from './GithubMultiOrgReaderProcessor'; -export { GitLabDiscoveryProcessor } from './GitLabDiscoveryProcessor'; export { LocationEntityProcessor } from './LocationEntityProcessor'; export type { LocationEntityProcessorOptions } from './LocationEntityProcessor'; export { PlaceholderProcessor } from './PlaceholderProcessor'; @@ -38,11 +28,5 @@ export type { PlaceholderResolverRead, PlaceholderResolverResolveUrl, } from './PlaceholderProcessor'; -export { StaticLocationProcessor } from './StaticLocationProcessor'; -export * from './types'; export { UrlReaderProcessor } from './UrlReaderProcessor'; -export { parseEntityYaml } from './util/parse'; -export { results }; - -export type { BitbucketRepositoryParser } from './bitbucket'; -export type { GithubMultiOrgConfig } from './github'; +export { parseEntityYaml } from '../util/parse'; diff --git a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.test.ts b/plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.test.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.test.ts rename to plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.test.ts index e7aec1520d..dd3bf490fb 100644 --- a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.test.ts +++ b/plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.test.ts @@ -20,10 +20,12 @@ import { GithubCredentialsProvider, GitHubIntegrationConfig, } from '@backstage/integration'; -import { GitHubOrgEntityProvider } from '.'; -import { EntityProviderConnection } from '../../providers'; -import { withLocations } from './GitHubOrgEntityProvider'; import { graphql } from '@octokit/graphql'; +import { EntityProviderConnection } from '../../api'; +import { + GitHubOrgEntityProvider, + withLocations, +} from './GitHubOrgEntityProvider'; jest.mock('@octokit/graphql'); diff --git a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts b/plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts rename to plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.ts index a20f7fdb0b..76389968ae 100644 --- a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts +++ b/plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, @@ -29,13 +30,13 @@ import { import { graphql } from '@octokit/graphql'; import { merge } from 'lodash'; import { Logger } from 'winston'; -import { EntityProvider, EntityProviderConnection } from '../../providers'; +import { EntityProvider, EntityProviderConnection } from '../../api'; import { getOrganizationTeams, getOrganizationUsers, parseGitHubOrgUrl, -} from '../processors/github'; -import { assignGroupsToUsers, buildOrgHierarchy } from '../processors/util/org'; +} from './lib'; +import { assignGroupsToUsers, buildOrgHierarchy } from '../util/org'; // TODO: Consider supporting an (optional) webhook that reacts on org changes /** @public */ diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.test.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts rename to plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.test.ts index 55e5560451..5440539b27 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.test.ts @@ -15,16 +15,16 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { GithubDiscoveryProcessor, parseUrl } from './GithubDiscoveryProcessor'; -import { getOrganizationRepositories } from './github'; -import { LocationSpec } from './types'; import { ConfigReader } from '@backstage/config'; import { - ScmIntegrations, DefaultGithubCredentialsProvider, + ScmIntegrations, } from '@backstage/integration'; +import { LocationSpec } from '../../api'; +import { GithubDiscoveryProcessor, parseUrl } from './GithubDiscoveryProcessor'; +import { getOrganizationRepositories } from './lib'; -jest.mock('./github'); +jest.mock('./lib'); const mockGetOrganizationRepositories = getOrganizationRepositories as jest.MockedFunction< typeof getOrganizationRepositories diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.ts similarity index 96% rename from plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts rename to plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.ts index e790134011..4aff470f52 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.ts @@ -23,9 +23,13 @@ import { } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; import { Logger } from 'winston'; -import { getOrganizationRepositories } from './github'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; +import { getOrganizationRepositories } from './lib'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; /** * Extracts repositories out of a GitHub org. @@ -141,7 +145,7 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { const path = `/blob/${branchName}${catalogPath}`; emit( - results.location({ + processingResult.location({ type: 'url', target: `${repository.url}${path}`, // Not all locations may actually exist, since the user defined them as a wildcard pattern. diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts b/plugins/catalog-backend/src/modules/github/GithubMultiOrgReaderProcessor.ts similarity index 95% rename from plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts rename to plugins/catalog-backend/src/modules/github/GithubMultiOrgReaderProcessor.ts index 383b82c019..dbd858fc0d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/github/GithubMultiOrgReaderProcessor.ts @@ -30,16 +30,21 @@ import { getOrganizationUsers, GithubMultiOrgConfig, readGithubMultiOrgConfig, -} from './github'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; -import { buildOrgHierarchy } from './util/org'; +} from './lib'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; +import { buildOrgHierarchy } from '../util/org'; /** - * @alpha * Extracts teams and users out of a multiple GitHub orgs namespaced per org. * * Be aware that this processor may not be compatible with future org structures in the catalog. + * + * @public */ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; @@ -158,7 +163,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { buildOrgHierarchy(groups); for (const group of groups) { - emit(results.entity(location, group)); + emit(processingResult.entity(location, group)); } } catch (e) { this.logger.error( @@ -169,7 +174,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { const allUsers = Array.from(allUsersMap.values()); for (const user of allUsers) { - emit(results.entity(location, user)); + emit(processingResult.entity(location, user)); } return true; diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.test.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts rename to plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.test.ts index c01db88d3a..9ce278ac44 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { @@ -21,7 +22,7 @@ import { } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; import { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; -import { LocationSpec } from './types'; +import { LocationSpec } from '../../api'; jest.mock('@octokit/graphql'); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.ts similarity index 92% rename from plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts rename to plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.ts index 8b5914cc80..e85e7b874d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.ts @@ -28,10 +28,14 @@ import { getOrganizationTeams, getOrganizationUsers, parseGitHubOrgUrl, -} from './github'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; -import { assignGroupsToUsers, buildOrgHierarchy } from './util/org'; +} from './lib'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; +import { assignGroupsToUsers, buildOrgHierarchy } from '../util/org'; type GraphQL = typeof graphql; @@ -106,10 +110,10 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { // Done! for (const group of groups) { - emit(results.entity(location, group)); + emit(processingResult.entity(location, group)); } for (const user of users) { - emit(results.entity(location, user)); + emit(processingResult.entity(location, user)); } return true; diff --git a/plugins/catalog-backend/src/modules/github/index.ts b/plugins/catalog-backend/src/modules/github/index.ts new file mode 100644 index 0000000000..7958818096 --- /dev/null +++ b/plugins/catalog-backend/src/modules/github/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 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. + */ + +export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor'; +export { GithubMultiOrgReaderProcessor } from './GithubMultiOrgReaderProcessor'; +export { GitHubOrgEntityProvider } from './GitHubOrgEntityProvider'; +export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; +export type { GithubMultiOrgConfig } from './lib'; diff --git a/plugins/catalog-backend/src/ingestion/processors/github/config.test.ts b/plugins/catalog-backend/src/modules/github/lib/config.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/github/config.test.ts rename to plugins/catalog-backend/src/modules/github/lib/config.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/github/config.ts b/plugins/catalog-backend/src/modules/github/lib/config.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/github/config.ts rename to plugins/catalog-backend/src/modules/github/lib/config.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts b/plugins/catalog-backend/src/modules/github/lib/github.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/github/github.test.ts rename to plugins/catalog-backend/src/modules/github/lib/github.test.ts index b38153a193..fb4caa42c0 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts +++ b/plugins/catalog-backend/src/modules/github/lib/github.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { graphql } from '@octokit/graphql'; import { graphql as graphqlMsw } from 'msw'; import { setupServer } from 'msw/node'; diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.ts b/plugins/catalog-backend/src/modules/github/lib/github.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/github/github.ts rename to plugins/catalog-backend/src/modules/github/lib/github.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/github/index.ts b/plugins/catalog-backend/src/modules/github/lib/index.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/github/index.ts rename to plugins/catalog-backend/src/modules/github/lib/index.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/github/util.test.ts b/plugins/catalog-backend/src/modules/github/lib/util.test.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/github/util.test.ts rename to plugins/catalog-backend/src/modules/github/lib/util.test.ts index 02f54e0675..c73c11bd2d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/util.test.ts +++ b/plugins/catalog-backend/src/modules/github/lib/util.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { parseGitHubOrgUrl } from './util'; describe('parseGitHubOrgUrl', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/github/util.ts b/plugins/catalog-backend/src/modules/github/lib/util.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/github/util.ts rename to plugins/catalog-backend/src/modules/github/lib/util.ts index d8df376038..a38225ff98 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/util.ts +++ b/plugins/catalog-backend/src/modules/github/lib/util.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export function parseGitHubOrgUrl(urlString: string): { org: string } { const path = new URL(urlString).pathname.substr(1).split('/'); diff --git a/plugins/catalog-backend/src/modules/index.ts b/plugins/catalog-backend/src/modules/index.ts new file mode 100644 index 0000000000..c541250c46 --- /dev/null +++ b/plugins/catalog-backend/src/modules/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2022 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. + */ + +export * from './bitbucket'; +export * from './codeowners'; +export * from './core'; +export * from './github'; diff --git a/plugins/catalog-backend/src/ingestion/processors/util/org.test.ts b/plugins/catalog-backend/src/modules/util/org.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/util/org.test.ts rename to plugins/catalog-backend/src/modules/util/org.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/util/org.ts b/plugins/catalog-backend/src/modules/util/org.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/util/org.ts rename to plugins/catalog-backend/src/modules/util/org.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts b/plugins/catalog-backend/src/modules/util/parse.test.ts similarity index 91% rename from plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts rename to plugins/catalog-backend/src/modules/util/parse.test.ts index a42d160725..e87de1c9a3 100644 --- a/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts +++ b/plugins/catalog-backend/src/modules/util/parse.test.ts @@ -15,7 +15,7 @@ */ import { parseEntityYaml } from './parse'; -import * as result from '../results'; +import { processingResult } from '../../api'; const testLoc = { target: 'my-loc-target', @@ -47,7 +47,7 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -92,7 +92,7 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -102,7 +102,7 @@ describe('parseEntityYaml', () => { type: 'website', }, }), - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -137,7 +137,7 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -157,7 +157,7 @@ describe('parseEntityYaml', () => { // Parse errors are always per document expect(results).toEqual([ - result.generalError( + processingResult.generalError( testLoc, 'YAML error at my-loc-type:my-loc-target, YAMLSemanticError: Plain value cannot start with reserved character `', ), @@ -186,7 +186,7 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -196,7 +196,7 @@ describe('parseEntityYaml', () => { type: 'website', }, }), - result.generalError( + processingResult.generalError( testLoc, 'YAML error at my-loc-type:my-loc-target, YAMLSemanticError: Nested mappings are not allowed in compact mappings', ), @@ -209,7 +209,10 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.generalError(testLoc, 'Expected object at root, got string'), + processingResult.generalError( + testLoc, + 'Expected object at root, got string', + ), ]); }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/util/parse.ts b/plugins/catalog-backend/src/modules/util/parse.ts similarity index 87% rename from plugins/catalog-backend/src/ingestion/processors/util/parse.ts rename to plugins/catalog-backend/src/modules/util/parse.ts index 0beaf1cd4c..65fad5986f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/util/parse.ts +++ b/plugins/catalog-backend/src/modules/util/parse.ts @@ -17,12 +17,12 @@ import { Entity, stringifyLocationRef } from '@backstage/catalog-model'; import lodash from 'lodash'; import yaml from 'yaml'; -import * as result from '../results'; import { CatalogProcessorParser, CatalogProcessorResult, LocationSpec, -} from '../types'; + processingResult, +} from '../../api'; /** @public */ export function* parseEntityYaml( @@ -35,7 +35,7 @@ export function* parseEntityYaml( } catch (e) { const loc = stringifyLocationRef(location); const message = `Failed to parse YAML at ${loc}, ${e}`; - yield result.generalError(location, message); + yield processingResult.generalError(location, message); return; } @@ -43,17 +43,17 @@ export function* parseEntityYaml( if (document.errors?.length) { const loc = stringifyLocationRef(location); const message = `YAML error at ${loc}, ${document.errors[0]}`; - yield result.generalError(location, message); + yield processingResult.generalError(location, message); } else { const json = document.toJSON(); if (lodash.isPlainObject(json)) { - yield result.entity(location, json as Entity); + yield processingResult.entity(location, json as Entity); } else if (json === null) { // Ignore null values, these happen if there is an empty document in the // YAML file, for example if --- is added to the end of the file. } else { const message = `Expected object at root, got ${typeof json}`; - yield result.generalError(location, message); + yield processingResult.generalError(location, message); } } } diff --git a/plugins/catalog-backend/src/permissions/conditionExports.ts b/plugins/catalog-backend/src/permissions/conditionExports.ts index 28e74c53c2..2846c8c3ec 100644 --- a/plugins/catalog-backend/src/permissions/conditionExports.ts +++ b/plugins/catalog-backend/src/permissions/conditionExports.ts @@ -27,7 +27,8 @@ const conditionExports = createConditionExports({ /** * These conditions are used when creating conditional decisions that are returned * by authorization policies. - * @public + * + * @alpha */ export const catalogConditions = conditionExports.conditions; @@ -50,7 +51,8 @@ export const catalogConditions = conditionExports.conditions; * } * } * ``` - * @public + * + * @alpha */ export const createCatalogPolicyDecision = conditionExports.createPolicyDecision; diff --git a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts index 81ebd52789..c6c5951153 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts @@ -21,7 +21,7 @@ import { createCatalogPermissionRule } from './util'; * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which * filters for the presence of an annotation on a given entity. * - * @public + * @alpha */ export const hasAnnotation = createCatalogPermissionRule({ name: 'HAS_ANNOTATION', diff --git a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts index 04b00d68fa..7296eecd89 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts @@ -20,7 +20,7 @@ import { createCatalogPermissionRule } from './util'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which * filters for entities with a specified label in its metadata. - * @public + * @alpha */ export const hasLabel = createCatalogPermissionRule({ name: 'HAS_LABEL', diff --git a/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts b/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts index f5f25a5ecf..cf14faea14 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts @@ -23,6 +23,6 @@ import { createPropertyRule } from './createPropertyRule'; * * The key argument to the `apply` and `toQuery` methods can be nested, such as * 'field.nestedfield'. - * @public + * @alpha */ export const hasMetadata = createPropertyRule('metadata'); diff --git a/plugins/catalog-backend/src/permissions/rules/hasSpec.ts b/plugins/catalog-backend/src/permissions/rules/hasSpec.ts index 891cf1d58c..63ae67d01c 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasSpec.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasSpec.ts @@ -23,6 +23,6 @@ import { createPropertyRule } from './createPropertyRule'; * * The key argument to the `apply` and `toQuery` methods can be nested, such as * 'field.nestedfield'. - * @public + * @alpha */ export const hasSpec = createPropertyRule('spec'); diff --git a/plugins/catalog-backend/src/permissions/rules/index.ts b/plugins/catalog-backend/src/permissions/rules/index.ts index 4eec796c74..f9d192781e 100644 --- a/plugins/catalog-backend/src/permissions/rules/index.ts +++ b/plugins/catalog-backend/src/permissions/rules/index.ts @@ -24,7 +24,8 @@ import { hasSpec } from './hasSpec'; /** * These permission rules can be used to conditionally filter catalog entities * or describe a user's access to the entities. - * @public + * + * @alpha */ export const permissionRules = { hasAnnotation, diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts index 6356c94dc4..c330c903d9 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts @@ -20,7 +20,7 @@ import { createCatalogPermissionRule } from './util'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which * filters for entities with a specified kind. - * @public + * @alpha */ export const isEntityKind = createCatalogPermissionRule({ name: 'IS_ENTITY_KIND', diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts index a6dda13c29..23f118abc4 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts @@ -21,7 +21,7 @@ import { createCatalogPermissionRule } from './util'; * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which * filters for entities with a specified owner. * - * @public + * @alpha */ export const isEntityOwner = createCatalogPermissionRule({ name: 'IS_ENTITY_OWNER', diff --git a/plugins/catalog-backend/src/permissions/rules/util.ts b/plugins/catalog-backend/src/permissions/rules/util.ts index 7ef0ca3546..a1316c7fb2 100644 --- a/plugins/catalog-backend/src/permissions/rules/util.ts +++ b/plugins/catalog-backend/src/permissions/rules/util.ts @@ -23,7 +23,7 @@ import { EntitiesSearchFilter } from '../../catalog/types'; * {@link @backstage/plugin-permission-node#PermissionRule}s for the * catalog-backend. * - * @public + * @alpha */ export const createCatalogPermissionRule = makeCreatePermissionRule< Entity, diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts index 0853484408..6e494d784e 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts @@ -29,11 +29,11 @@ import { CatalogProcessorEmit, CatalogProcessorParser, LocationSpec, - results, -} from '../ingestion'; + processingResult, +} from '../api'; import { CatalogRulesEnforcer } from '../ingestion/CatalogRules'; import { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator'; -import { defaultEntityDataParser } from '../ingestion/processors/util/parse'; +import { defaultEntityDataParser } from '../modules/util/parse'; import { ConfigReader } from '@backstage/config'; class FooBarProcessor implements CatalogProcessor { @@ -51,7 +51,7 @@ class FooBarProcessor implements CatalogProcessor { ) { if (await cache.get('emit')) { emit( - results.entity( + processingResult.entity( { type: 'url', target: './new-place' }, { apiVersion: 'my-api/v1', @@ -63,7 +63,7 @@ class FooBarProcessor implements CatalogProcessor { ), ); emit( - results.relation({ + processingResult.relation({ type: 'my-type', source: { kind: 'foobar', name: 'my-source', namespace: 'default' }, target: { kind: 'foobar', name: 'my-target', namespace: 'default' }, @@ -211,7 +211,7 @@ describe('DefaultCatalogProcessingOrchestrator', () => { getProcessorName: jest.fn(), validateEntityKind: jest.fn(async () => true), readLocation: jest.fn(async (_l, _o, emit) => { - emit(results.entity({ type: 't', target: 't' }, entity)); + emit(processingResult.entity({ type: 't', target: 't' }, entity)); return true; }), }; diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index 9649bc937b..c050779a8b 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -36,8 +36,8 @@ import { CatalogProcessor, CatalogProcessorParser, LocationSpec, -} from '../ingestion/processors'; -import * as results from '../ingestion/processors/results'; + processingResult, +} from '../api'; import { CatalogProcessingOrchestrator, EntityProcessingRequest, @@ -178,13 +178,13 @@ export class DefaultCatalogProcessingOrchestrator entity: Entity, context: Context, ): Promise { - let result = entity; + let res = entity; for (const processor of this.options.processors) { if (processor.preProcessEntity) { try { - result = await processor.preProcessEntity( - result, + res = await processor.preProcessEntity( + res, context.location, context.collector.onEmit, context.originLocation, @@ -199,7 +199,7 @@ export class DefaultCatalogProcessingOrchestrator } } - return result; + return res; } /** @@ -295,7 +295,7 @@ export class DefaultCatalogProcessingOrchestrator for (const maybeRelativeTarget of targets) { if (type === 'file' && maybeRelativeTarget.endsWith(path.sep)) { context.collector.onEmit( - results.inputError( + processingResult.inputError( context.location, `LocationEntityProcessor cannot handle ${type} type location with target ${context.location.target} that ends with a path separator`, ), @@ -351,13 +351,13 @@ export class DefaultCatalogProcessingOrchestrator entity: Entity, context: Context, ): Promise { - let result = entity; + let res = entity; for (const processor of this.options.processors) { if (processor.postProcessEntity) { try { - result = await processor.postProcessEntity( - result, + res = await processor.postProcessEntity( + res, context.location, context.collector.onEmit, context.cache.forProcessor(processor), @@ -371,6 +371,6 @@ export class DefaultCatalogProcessingOrchestrator } } - return result; + return res; } } diff --git a/plugins/catalog-backend/src/processing/ProcessorCacheManager.test.ts b/plugins/catalog-backend/src/processing/ProcessorCacheManager.test.ts index 71631b13d8..602557a224 100644 --- a/plugins/catalog-backend/src/processing/ProcessorCacheManager.test.ts +++ b/plugins/catalog-backend/src/processing/ProcessorCacheManager.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { CatalogProcessor } from '../ingestion/processors'; +import { CatalogProcessor } from '../api'; import { ProcessorCacheManager } from './ProcessorCacheManager'; class MyProcessor implements CatalogProcessor { diff --git a/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts b/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts index ab95fcedb7..e54641f572 100644 --- a/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts +++ b/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts @@ -15,8 +15,7 @@ */ import { JsonObject, JsonValue } from '@backstage/types'; -import { CatalogProcessor } from '../ingestion/processors'; -import { CatalogProcessorCache } from '../ingestion/processors/types'; +import { CatalogProcessor, CatalogProcessorCache } from '../api'; import { isObject } from './util'; class SingleProcessorSubCache implements CatalogProcessorCache { diff --git a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts index 351e68882b..cc3e1401bd 100644 --- a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts +++ b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts @@ -22,9 +22,9 @@ import { } from '@backstage/catalog-model'; import { assertError } from '@backstage/errors'; import { Logger } from 'winston'; -import { CatalogProcessorResult } from '../ingestion'; +import { CatalogProcessorResult, EntityRelationSpec } from '../api'; import { locationSpecToLocationEntity } from '../util/conversion'; -import { DeferredEntity, EntityRelationSpec } from './types'; +import { DeferredEntity } from './types'; import { getEntityLocationRef, getEntityOriginLocationRef, @@ -72,17 +72,17 @@ export class ProcessorOutputCollector { if (i.type === 'entity') { let entity: Entity; + const location = stringifyLocationRef(i.location); + try { entity = validateEntityEnvelope(i.entity); } catch (e) { assertError(e); - this.logger.debug(`Envelope validation failed at ${i.location}, ${e}`); + this.logger.debug(`Envelope validation failed at ${location}, ${e}`); this.errors.push(e); return; } - const location = stringifyLocationRef(i.location); - // Note that at this point, we have only validated the envelope part of // the entity data. Annotations are not part of that, so we have to be // defensive. If the annotations were malformed (e.g. were not a valid @@ -106,9 +106,8 @@ export class ProcessorOutputCollector { this.deferredEntities.push({ entity, locationKey: location }); } else if (i.type === 'location') { - const presence = i.optional ? 'optional' : 'required'; const entity = locationSpecToLocationEntity( - { presence, ...i.location }, + i.location, this.parentEntity, ); const locationKey = getEntityLocationRef(entity); diff --git a/plugins/catalog-backend/src/processing/connectEntityProviders.ts b/plugins/catalog-backend/src/processing/connectEntityProviders.ts index 02bdc2aee3..d7015ea52e 100644 --- a/plugins/catalog-backend/src/processing/connectEntityProviders.ts +++ b/plugins/catalog-backend/src/processing/connectEntityProviders.ts @@ -23,7 +23,7 @@ import { EntityProvider, EntityProviderConnection, EntityProviderMutation, -} from '../providers/types'; +} from '../api'; class Connection implements EntityProviderConnection { readonly validateEntityEnvelope = entityEnvelopeSchemaValidator(); diff --git a/plugins/catalog-backend/src/processing/index.ts b/plugins/catalog-backend/src/processing/index.ts index 83f8a9f566..aec6a5d71c 100644 --- a/plugins/catalog-backend/src/processing/index.ts +++ b/plugins/catalog-backend/src/processing/index.ts @@ -19,10 +19,15 @@ export type { CatalogProcessingEngine, EntityProcessingRequest, EntityProcessingResult, - EntityRelationSpec, DeferredEntity, } from './types'; export { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator'; -export { createRandomRefreshInterval } from './refresh'; -export type { RefreshIntervalFunction } from './refresh'; +export { + createRandomRefreshInterval, + createRandomProcessingInterval, +} from './refresh'; +export type { + RefreshIntervalFunction, + ProcessingIntervalFunction, +} from './refresh'; diff --git a/plugins/catalog-backend/src/processing/refresh.ts b/plugins/catalog-backend/src/processing/refresh.ts index 3e03e3add8..941c338912 100644 --- a/plugins/catalog-backend/src/processing/refresh.ts +++ b/plugins/catalog-backend/src/processing/refresh.ts @@ -16,13 +16,21 @@ /** * Function that returns the catalog refresh interval in seconds. + * @deprecated use {@link ProcessingIntervalFunction} instead * @public */ export type RefreshIntervalFunction = () => number; +/** + * Function that returns the catalog processing interval in seconds. + * @public + */ +export type ProcessingIntervalFunction = () => number; + /** * Creates a function that returns a random refresh interval between minSeconds and maxSeconds. * @returns A {@link RefreshIntervalFunction} that provides the next refresh interval + * @deprecated use {@link createRandomProcessingInterval} instead * @public */ export function createRandomRefreshInterval(options: { @@ -34,3 +42,18 @@ export function createRandomRefreshInterval(options: { return Math.random() * (maxSeconds - minSeconds) + minSeconds; }; } + +/** + * Creates a function that returns a random processing interval between minSeconds and maxSeconds. + * @returns A {@link ProcessingIntervalFunction} that provides the next processing interval + * @public + */ +export function createRandomProcessingInterval(options: { + minSeconds: number; + maxSeconds: number; +}): ProcessingIntervalFunction { + const { minSeconds, maxSeconds } = options; + return () => { + return Math.random() * (maxSeconds - minSeconds) + minSeconds; + }; +} diff --git a/plugins/catalog-backend/src/processing/types.ts b/plugins/catalog-backend/src/processing/types.ts index b732df297e..b178522c14 100644 --- a/plugins/catalog-backend/src/processing/types.ts +++ b/plugins/catalog-backend/src/processing/types.ts @@ -14,30 +14,9 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; - -/** - * Holds the relation data for entities. - * - * @public - */ -export type EntityRelationSpec = { - /** - * The source entity of this relation. - */ - source: EntityName; - - /** - * The type of the relation. - */ - type: string; - - /** - * The target entity of this relation. - */ - target: EntityName; -}; +import { EntityRelationSpec } from '../api'; /** * The request to process an entity. @@ -69,7 +48,7 @@ export type EntityProcessingResult = /** * Responsible for executing the individual processing steps in order to fully process an entity. * @public - * */ + */ export interface CatalogProcessingOrchestrator { process(request: EntityProcessingRequest): Promise; } @@ -77,7 +56,7 @@ export interface CatalogProcessingOrchestrator { /** * Entities that are not yet processed. * @public - * */ + */ export type DeferredEntity = { entity: Entity; locationKey?: string; diff --git a/plugins/catalog-backend/src/processing/util.ts b/plugins/catalog-backend/src/processing/util.ts index 94634f794a..b1948cf559 100644 --- a/plugins/catalog-backend/src/processing/util.ts +++ b/plugins/catalog-backend/src/processing/util.ts @@ -27,7 +27,7 @@ import { JsonObject, JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import path from 'path'; -import { LocationSpec } from '../ingestion'; +import { LocationSpec } from '../api'; export function isLocationEntity(entity: Entity): entity is LocationEntity { return entity.kind === 'Location'; diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index 96704a6c1a..ac9a33bf62 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -23,26 +23,23 @@ import { stringifyEntityRef, UserEntity, } from '@backstage/catalog-model'; -import { IndexableDocument, DocumentCollator } from '@backstage/search-common'; import { Config } from '@backstage/config'; import { CatalogApi, CatalogClient, GetEntitiesRequest, } from '@backstage/catalog-client'; -import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; +import { + catalogEntityReadPermission, + CatalogEntityDocument, +} from '@backstage/plugin-catalog-common'; -/** @public */ -export interface CatalogEntityDocument extends IndexableDocument { - componentType: string; - namespace: string; - kind: string; - lifecycle: string; - owner: string; -} - -/** @public */ -export class DefaultCatalogCollator implements DocumentCollator { +/** + * @public + * @deprecated Upgrade to a more recent `@backstage/search-backend-node` and + * use `DefaultCatalogCollatorFactory` instead. + */ +export class DefaultCatalogCollator { protected discovery: PluginEndpointDiscovery; protected locationTemplate: string; protected filter?: GetEntitiesRequest['filter']; @@ -130,6 +127,7 @@ export class DefaultCatalogCollator implements DocumentCollator { }), text: this.getDocumentText(entity), componentType: entity.spec?.type?.toString() || 'other', + type: entity.spec?.type?.toString() || 'other', namespace: entity.metadata.namespace || 'default', kind: entity.kind, lifecycle: (entity.spec?.lifecycle as string) || '', diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts new file mode 100644 index 0000000000..ccb61b8680 --- /dev/null +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts @@ -0,0 +1,213 @@ +/* + * Copyright 2022 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 { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { TestPipeline } from '@backstage/plugin-search-backend-node'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { Readable } from 'stream'; +import { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory'; + +const server = setupServer(); + +const expectedEntities: Entity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-entity', + description: 'The expected description', + }, + spec: { + type: 'some-type', + lifecycle: 'experimental', + owner: 'someone', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + title: 'Test Entity', + name: 'test-entity-2', + description: 'The expected description 2', + }, + spec: { + type: 'some-type', + lifecycle: 'experimental', + owner: 'someone', + }, + }, +]; + +describe('DefaultCatalogCollatorFactory', () => { + const config = new ConfigReader({}); + const mockDiscoveryApi: jest.Mocked = { + getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007'), + getExternalBaseUrl: jest.fn(), + }; + const mockTokenManager: jest.Mocked = { + getToken: jest.fn().mockResolvedValue({ token: '' }), + authenticate: jest.fn(), + }; + const options = { + discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, + }; + + beforeAll(() => { + server.listen(); + }); + + beforeEach(() => { + server.use( + rest.get('http://localhost:7007/entities', (req, res, ctx) => { + if (req.url.searchParams.has('filter')) { + const filter = req.url.searchParams.get('filter'); + if (filter === 'kind=Foo,kind=Bar') { + // When filtering on the 'Foo,Bar' kinds we simply return no items, to simulate a filter + return res(ctx.json([])); + } + throw new Error('Unexpected filter parameter'); + } + + // Imitate offset/limit pagination. + const offset = parseInt(req.url.searchParams.get('offset') || '0', 10); + const limit = parseInt(req.url.searchParams.get('limit') || '500', 10); + return res(ctx.json(expectedEntities.slice(offset, limit + offset))); + }), + ); + }); + + afterAll(() => { + server.close(); + }); + + afterEach(() => server.resetHandlers()); + + it('has expected type', () => { + const factory = DefaultCatalogCollatorFactory.fromConfig(config, options); + expect(factory.type).toBe('software-catalog'); + }); + + describe('getCollator', () => { + let factory: DefaultCatalogCollatorFactory; + let collator: Readable; + + beforeEach(async () => { + factory = DefaultCatalogCollatorFactory.fromConfig(config, options); + collator = await factory.getCollator(); + }); + + it('returns a readable stream', async () => { + expect(collator).toBeInstanceOf(Readable); + }); + + it('fetches from the configured catalog service', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('catalog'); + expect(documents).toHaveLength(expectedEntities.length); + }); + + it('maps a returned entity to an expected CatalogEntityDocument', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + expect(documents[0]).toMatchObject({ + title: expectedEntities[0].metadata.name, + location: '/catalog/default/component/test-entity', + text: expectedEntities[0].metadata.description, + namespace: 'default', + componentType: expectedEntities[0]!.spec!.type, + lifecycle: expectedEntities[0]!.spec!.lifecycle, + owner: expectedEntities[0]!.spec!.owner, + authorization: { + resourceRef: 'component:default/test-entity', + }, + }); + expect(documents[1]).toMatchObject({ + title: expectedEntities[1].metadata.title, + location: '/catalog/default/component/test-entity-2', + text: expectedEntities[1].metadata.description, + namespace: 'default', + componentType: expectedEntities[1]!.spec!.type, + lifecycle: expectedEntities[1]!.spec!.lifecycle, + owner: expectedEntities[1]!.spec!.owner, + authorization: { + resourceRef: 'component:default/test-entity-2', + }, + }); + }); + + it('maps a returned entity with a custom locationTemplate', async () => { + // Provide an alternate location template. + factory = DefaultCatalogCollatorFactory.fromConfig(new ConfigReader({}), { + discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, + locationTemplate: '/software/:name', + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + expect(documents[0]).toMatchObject({ + location: '/software/test-entity', + }); + }); + + it('allows filtering of the retrieved catalog entities', async () => { + // Provide a custom filter. + factory = DefaultCatalogCollatorFactory.fromConfig(new ConfigReader({}), { + discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, + filter: { + kind: ['Foo', 'Bar'], + }, + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + // The simulated 'Foo,Bar' filter should return in an empty list + expect(documents).toHaveLength(0); + }); + + it('paginates through catalog entities using batchSize', async () => { + factory = DefaultCatalogCollatorFactory.fromConfig(config, { + ...options, + batchSize: 1, + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + expect(documents).toHaveLength(expectedEntities.length); + expect(documents[0].location).toBe( + '/catalog/default/component/test-entity', + ); + expect(documents[1].location).toBe( + '/catalog/default/component/test-entity-2', + ); + }); + }); +}); diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts new file mode 100644 index 0000000000..4d28559c82 --- /dev/null +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts @@ -0,0 +1,165 @@ +/* + * Copyright 2022 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 { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { + CatalogApi, + CatalogClient, + GetEntitiesRequest, +} from '@backstage/catalog-client'; +import { + Entity, + stringifyEntityRef, + UserEntity, +} from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { + catalogEntityReadPermission, + CatalogEntityDocument, +} from '@backstage/plugin-catalog-common'; +import { Readable } from 'stream'; + +/** @public */ +export type DefaultCatalogCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + locationTemplate?: string; + filter?: GetEntitiesRequest['filter']; + batchSize?: number; + catalogClient?: CatalogApi; +}; + +/** @public */ +export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'software-catalog'; + public readonly visibilityPermission = catalogEntityReadPermission; + + private locationTemplate: string; + private filter?: GetEntitiesRequest['filter']; + private batchSize: number; + private readonly catalogClient: CatalogApi; + private tokenManager: TokenManager; + + static fromConfig( + _config: Config, + options: DefaultCatalogCollatorFactoryOptions, + ) { + return new DefaultCatalogCollatorFactory(options); + } + + private constructor(options: DefaultCatalogCollatorFactoryOptions) { + const { + batchSize, + discovery, + locationTemplate, + filter, + catalogClient, + tokenManager, + } = options; + + this.locationTemplate = + locationTemplate || '/catalog/:namespace/:kind/:name'; + this.filter = filter; + this.batchSize = batchSize || 500; + this.catalogClient = + catalogClient || new CatalogClient({ discoveryApi: discovery }); + this.tokenManager = tokenManager; + } + + async getCollator(): Promise { + return Readable.from(this.execute()); + } + + private applyArgsToFormat( + format: string, + args: Record, + ): string { + let formatted = format; + for (const [key, value] of Object.entries(args)) { + formatted = formatted.replace(`:${key}`, value); + } + return formatted.toLowerCase(); + } + + private isUserEntity(entity: Entity): entity is UserEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'USER'; + } + + private getDocumentText(entity: Entity): string { + let documentText = entity.metadata.description || ''; + if (this.isUserEntity(entity)) { + if (entity.spec?.profile?.displayName && documentText) { + // combine displayName and description + const displayName = entity.spec?.profile?.displayName; + documentText = displayName.concat(' : ', documentText); + } else { + documentText = entity.spec?.profile?.displayName || documentText; + } + } + return documentText; + } + + private async *execute(): AsyncGenerator { + const { token } = await this.tokenManager.getToken(); + let entitiesRetrieved = 0; + let moreEntitiesToGet = true; + + // Offset/limit pagination is used on the Catalog Client in order to + // limit (and allow some control over) memory used by the search backend + // at index-time. + while (moreEntitiesToGet) { + const entities = ( + await this.catalogClient.getEntities( + { + filter: this.filter, + limit: this.batchSize, + offset: entitiesRetrieved, + }, + { token }, + ) + ).items; + + // Control looping through entity batches. + moreEntitiesToGet = entities.length === this.batchSize; + entitiesRetrieved += entities.length; + + for (const entity of entities) { + yield { + title: entity.metadata.title ?? entity.metadata.name, + location: this.applyArgsToFormat(this.locationTemplate, { + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + name: entity.metadata.name, + }), + text: this.getDocumentText(entity), + componentType: entity.spec?.type?.toString() || 'other', + type: entity.spec?.type?.toString() || 'other', + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + lifecycle: (entity.spec?.lifecycle as string) || '', + owner: (entity.spec?.owner as string) || '', + authorization: { + resourceRef: stringifyEntityRef(entity), + }, + }; + } + } + } +} diff --git a/plugins/catalog-backend/src/search/index.ts b/plugins/catalog-backend/src/search/index.ts index 5641670333..eb5b1e7c22 100644 --- a/plugins/catalog-backend/src/search/index.ts +++ b/plugins/catalog-backend/src/search/index.ts @@ -14,5 +14,18 @@ * limitations under the License. */ +export { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory'; +export type { DefaultCatalogCollatorFactoryOptions } from './DefaultCatalogCollatorFactory'; + +import { CatalogEntityDocument as CatalogEntityDocumentType } from '@backstage/plugin-catalog-common'; + +/** + * @deprecated import from `@backstage/plugin-catalog-common` instead + * @public + */ +export type CatalogEntityDocument = CatalogEntityDocumentType; + +/** + * todo(backstage/techdocs-core): stop exporting this in a future release. + */ export { DefaultCatalogCollator } from './DefaultCatalogCollator'; -export type { CatalogEntityDocument } from './DefaultCatalogCollator'; diff --git a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts index 819451d854..17dfb58c54 100644 --- a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts +++ b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { NotAllowedError } from '@backstage/errors'; import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common'; import { diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 9004cb4c56..768118404e 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -38,44 +38,45 @@ import { Router } from 'express'; import lodash, { keyBy } from 'lodash'; import { EntitiesCatalog, EntitiesSearchFilter } from '../catalog'; +import { + CatalogProcessor, + CatalogProcessorParser, + EntityProvider, +} from '../api'; import { AnnotateLocationEntityProcessor, BitbucketDiscoveryProcessor, BuiltinKindsEntityProcessor, - CatalogProcessor, - CatalogProcessorParser, CodeOwnersProcessor, FileReaderProcessor, - AzureDevOpsDiscoveryProcessor, GithubDiscoveryProcessor, GithubOrgReaderProcessor, - GitLabDiscoveryProcessor, PlaceholderProcessor, PlaceholderResolver, UrlReaderProcessor, -} from '../ingestion'; +} from '../modules'; +import { ConfigLocationEntityProvider } from '../modules/core/ConfigLocationEntityProvider'; +import { DefaultLocationStore } from '../modules/core/DefaultLocationStore'; import { RepoLocationAnalyzer } from '../ingestion/LocationAnalyzer'; import { jsonPlaceholderResolver, textPlaceholderResolver, yamlPlaceholderResolver, -} from '../ingestion/processors/PlaceholderProcessor'; -import { defaultEntityDataParser } from '../ingestion/processors/util/parse'; +} from '../modules/core/PlaceholderProcessor'; +import { defaultEntityDataParser } from '../modules/util/parse'; import { LocationAnalyzer } from '../ingestion/types'; -import { EntityProvider } from '../providers/types'; import { CatalogProcessingEngine } from '../processing/types'; -import { ConfigLocationEntityProvider } from '../providers/ConfigLocationEntityProvider'; import { DefaultProcessingDatabase } from '../database/DefaultProcessingDatabase'; import { applyDatabaseMigrations } from '../database/migrations'; import { DefaultCatalogProcessingEngine } from '../processing/DefaultCatalogProcessingEngine'; import { DefaultLocationService } from './DefaultLocationService'; -import { DefaultLocationStore } from '../providers/DefaultLocationStore'; import { DefaultEntitiesCatalog } from './DefaultEntitiesCatalog'; import { DefaultCatalogProcessingOrchestrator } from '../processing/DefaultCatalogProcessingOrchestrator'; import { Stitcher } from '../stitching/Stitcher'; import { - createRandomRefreshInterval, + createRandomProcessingInterval, RefreshIntervalFunction, + ProcessingIntervalFunction, } from '../processing/refresh'; import { createRouter } from './createRouter'; import { DefaultRefreshService } from './DefaultRefreshService'; @@ -137,8 +138,8 @@ export class CatalogBuilder { private processors: CatalogProcessor[]; private processorsReplace: boolean; private parser: CatalogProcessorParser | undefined; - private refreshInterval: RefreshIntervalFunction = - createRandomRefreshInterval({ + private processingInterval: ProcessingIntervalFunction = + createRandomProcessingInterval({ minSeconds: 100, maxSeconds: 150, }); @@ -190,9 +191,28 @@ export class CatalogBuilder { * Seconds provided will be multiplied by 1.5 * The default refresh duration is 100-150 seconds. * setting this too low will potentially deplete request quotas to upstream services. + * + * @deprecated use {@link CatalogBuilder#setProcessingIntervalSeconds} instead */ setRefreshIntervalSeconds(seconds: number): CatalogBuilder { - this.refreshInterval = createRandomRefreshInterval({ + this.env.logger.warn( + '[DEPRECATION] - CatalogBuilder.setRefreshIntervalSeconds is deprecated. Use CatalogBuilder.setProcessingIntervalSeconds instead.', + ); + this.processingInterval = createRandomProcessingInterval({ + minSeconds: seconds, + maxSeconds: seconds * 1.5, + }); + return this; + } + + /** + * Processing interval determines how often entities should be processed. + * Seconds provided will be multiplied by 1.5 + * The default processing interval is 100-150 seconds. + * setting this too low will potentially deplete request quotas to upstream services. + */ + setProcessingIntervalSeconds(seconds: number): CatalogBuilder { + this.processingInterval = createRandomProcessingInterval({ minSeconds: seconds, maxSeconds: seconds * 1.5, }); @@ -202,9 +222,25 @@ export class CatalogBuilder { /** * Overwrites the default refresh interval function used to spread * entity updates in the catalog. + * + * @deprecated use {@link CatalogBuilder#setProcessingInterval} instead */ setRefreshInterval(refreshInterval: RefreshIntervalFunction): CatalogBuilder { - this.refreshInterval = refreshInterval; + this.env.logger.warn( + '[DEPRECATION] - CatalogBuilder.setRefreshInterval is deprecated. Use CatalogBuilder.setProcessingInterval instead.', + ); + this.processingInterval = refreshInterval; + return this; + } + + /** + * Overwrites the default processing interval function used to spread + * entity updates in the catalog. + */ + setProcessingInterval( + processingInterval: ProcessingIntervalFunction, + ): CatalogBuilder { + this.processingInterval = processingInterval; return this; } @@ -322,7 +358,6 @@ export class CatalogBuilder { return [ new FileReaderProcessor(), BitbucketDiscoveryProcessor.fromConfig(config, { logger }), - AzureDevOpsDiscoveryProcessor.fromConfig(config, { logger }), GithubDiscoveryProcessor.fromConfig(config, { logger, githubCredentialsProvider, @@ -331,7 +366,6 @@ export class CatalogBuilder { logger, githubCredentialsProvider, }), - GitLabDiscoveryProcessor.fromConfig(config, { logger }), new UrlReaderProcessor({ reader, logger }), CodeOwnersProcessor.fromConfig(config, { logger, reader }), new AnnotateLocationEntityProcessor({ integrations }), @@ -394,7 +428,7 @@ export class CatalogBuilder { const processingDatabase = new DefaultProcessingDatabase({ database: dbClient, logger, - refreshInterval: this.refreshInterval, + refreshInterval: this.processingInterval, }); const integrations = ScmIntegrations.fromConfig(config); const rulesEnforcer = DefaultCatalogRulesEnforcer.fromConfig(config); diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.ts b/plugins/catalog-backend/src/service/DefaultLocationService.ts index c6b5eaf162..7a1b923068 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity, ANNOTATION_LOCATION, diff --git a/plugins/catalog-backend/src/service/types.ts b/plugins/catalog-backend/src/service/types.ts index 02268ab63c..fc315b9d89 100644 --- a/plugins/catalog-backend/src/service/types.ts +++ b/plugins/catalog-backend/src/service/types.ts @@ -77,7 +77,8 @@ export interface RefreshService { /** * Interacts with the database to manage locations. - * @public */ + * @public + */ export interface LocationStore { createLocation(location: LocationInput): Promise; listLocations(): Promise; diff --git a/plugins/catalog-backend/src/util/conversion.ts b/plugins/catalog-backend/src/util/conversion.ts index 75ed4f5e96..63771d1028 100644 --- a/plugins/catalog-backend/src/util/conversion.ts +++ b/plugins/catalog-backend/src/util/conversion.ts @@ -23,7 +23,7 @@ import { stringifyLocationRef, } from '@backstage/catalog-model'; import { createHash } from 'crypto'; -import { LocationSpec } from '../ingestion'; +import { LocationSpec } from '../api'; export function locationSpecToMetadataName(location: LocationSpec) { const hash = createHash('sha1') diff --git a/plugins/catalog-backend/src/util/index.ts b/plugins/catalog-backend/src/util/index.ts index 13a9c84b0f..0b5a942e0c 100644 --- a/plugins/catalog-backend/src/util/index.ts +++ b/plugins/catalog-backend/src/util/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export * from './runPeriodically'; export * from './RecursivePartial'; diff --git a/plugins/catalog-backend/src/util/runPeriodically.ts b/plugins/catalog-backend/src/util/runPeriodically.ts deleted file mode 100644 index 361fb33b2f..0000000000 --- a/plugins/catalog-backend/src/util/runPeriodically.ts +++ /dev/null @@ -1,56 +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. - */ - -/** - * Runs a function repeatedly, with a fixed wait between invocations. - * - * Supports async functions, and silently ignores exceptions and rejections. - * - * @param fn - The function to run. May return a Promise. - * @param delayMs - The delay between a completed function invocation and the - * next. - * @returns A function that, when called, stops the invocation loop. - * @deprecated use \@backstage/backend-tasks package instead. - * @public - */ -export function runPeriodically(fn: () => any, delayMs: number): () => void { - let cancel: () => void; - let cancelled = false; - const cancellationPromise = new Promise(resolve => { - cancel = () => { - resolve(); - cancelled = true; - }; - }); - - const startRefresh = async () => { - while (!cancelled) { - try { - await fn(); - } catch { - // ignore intentionally - } - - await Promise.race([ - new Promise(resolve => setTimeout(resolve, delayMs)), - cancellationPromise, - ]); - } - }; - startRefresh(); - - return cancel!; -} diff --git a/plugins/catalog-common/.eslintrc.js b/plugins/catalog-common/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/catalog-common/.eslintrc.js +++ b/plugins/catalog-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index 337bd6854c..be795cc898 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-catalog-common +## 0.2.2-next.0 + +### Patch Changes + +- b1aacbf96a: Applied the fix for the `/alpha` entry point resolution that was part of the `v0.70.1` release of Backstage. +- Updated dependencies + - @backstage/search-common@0.3.1-next.0 + +## 0.2.1 + +### Patch Changes + +- Fixed runtime resolution of the `/alpha` entry point. + +## 0.2.0 + +### Minor Changes + +- e3c2bfef11: Remove resourceType property from catalogEntityCreatePermission. Resource type refers to the type of resources whose resourceRefs should be passed along with authorize requests, to allow conditional responses for that resource type. Since creation does not correspond to an entity (as the entity does not exist at the time of authorization), the resourceRef should not be included on the permission. +- 81273e95cf: **Breaking**: Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-common/alpha` instead of `@backstage/plugin-catalog-common`. + + - `RESOURCE_TYPE_CATALOG_ENTITY` + - `catalogEntityReadPermission` + - `catalogEntityCreatePermission` + - `catalogEntityDeletePermission` + - `catalogEntityRefreshPermission` + - `catalogLocationReadPermission` + - `catalogLocationCreatePermission` + - `catalogLocationDeletePermission` + +### Patch Changes + +- ab7b6cb7b1: **DEPRECATION**: Moved the `CatalogEntityDocument` to `@backstage/plugin-catalog-common` and deprecated the export from `@backstage/plugin-catalog-backend`. + + A new `type` field has also been added to `CatalogEntityDocument` as a replacement for `componentType`, which is now deprecated. Both fields are still present and should be set to the same value in order to avoid issues with indexing. + + Any search customizations need to be updated to use this new `type` field instead, including any custom frontend filters, custom frontend result components, custom search decorators, or non-default Catalog collator implementations. + +- Updated dependencies + - @backstage/plugin-permission-common@0.5.2 + - @backstage/search-common@0.3.0 + ## 0.1.4 ### Patch Changes diff --git a/plugins/catalog-common/api-report.md b/plugins/catalog-common/api-report.md index 9826a8ddef..a73cae9994 100644 --- a/plugins/catalog-common/api-report.md +++ b/plugins/catalog-common/api-report.md @@ -3,29 +3,46 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { IndexableDocument } from '@backstage/search-common'; import { Permission } from '@backstage/plugin-permission-common'; -// @public +// @alpha export const catalogEntityCreatePermission: Permission; -// @public +// @alpha export const catalogEntityDeletePermission: Permission; // @public +export interface CatalogEntityDocument extends IndexableDocument { + // @deprecated (undocumented) + componentType: string; + // (undocumented) + kind: string; + // (undocumented) + lifecycle: string; + // (undocumented) + namespace: string; + // (undocumented) + owner: string; + // (undocumented) + type: string; +} + +// @alpha export const catalogEntityReadPermission: Permission; -// @public +// @alpha export const catalogEntityRefreshPermission: Permission; -// @public +// @alpha export const catalogLocationCreatePermission: Permission; -// @public +// @alpha export const catalogLocationDeletePermission: Permission; -// @public +// @alpha export const catalogLocationReadPermission: Permission; -// @public (undocumented) +// @alpha export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; ``` diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index 9a52d22798..9a6b11e517 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-common", "description": "Common functionalities for the catalog plugin", - "version": "0.1.4", + "version": "0.2.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,7 +10,8 @@ "access": "public", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "common-library" @@ -25,7 +26,7 @@ "backstage" ], "scripts": { - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -33,12 +34,14 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/plugin-permission-common": "^0.5.1" + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/search-common": "^0.3.1-next.0" }, "devDependencies": { - "@backstage/cli": "^0.14.0" + "@backstage/cli": "^0.15.2-next.0" }, "files": [ - "dist" + "dist", + "alpha" ] } diff --git a/plugins/catalog-common/src/index.ts b/plugins/catalog-common/src/index.ts index 9eddf37452..08184eb871 100644 --- a/plugins/catalog-common/src/index.ts +++ b/plugins/catalog-common/src/index.ts @@ -31,3 +31,5 @@ export { catalogLocationCreatePermission, catalogLocationDeletePermission, } from './permissions'; + +export * from './search'; diff --git a/plugins/catalog-common/src/permissions.ts b/plugins/catalog-common/src/permissions.ts index 52c225e441..31a8f32118 100644 --- a/plugins/catalog-common/src/permissions.ts +++ b/plugins/catalog-common/src/permissions.ts @@ -17,8 +17,10 @@ import { Permission } from '@backstage/plugin-permission-common'; /** + * Permission resource type which corresponds to catalog entities. + * * {@link https://backstage.io/docs/features/software-catalog/software-catalog-overview} - * @public + * @alpha */ export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; @@ -28,7 +30,7 @@ export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; * * If this permission is not authorized, it will appear that the entity does not * exist in the catalog — both in the frontend and in API responses. - * @public + * @alpha */ export const catalogEntityReadPermission: Permission = { name: 'catalog.entity.read', @@ -42,20 +44,19 @@ export const catalogEntityReadPermission: Permission = { * This permission is used to authorize actions that involve creating a new * catalog entity. This includes registering an existing component into the * catalog. - * @public + * @alpha */ export const catalogEntityCreatePermission: Permission = { name: 'catalog.entity.create', attributes: { action: 'create', }, - resourceType: RESOURCE_TYPE_CATALOG_ENTITY, }; /** * This permission is used to designate actions that involve removing one or * more entities from the catalog. - * @public + * @alpha */ export const catalogEntityDeletePermission: Permission = { name: 'catalog.entity.delete', @@ -68,7 +69,7 @@ export const catalogEntityDeletePermission: Permission = { /** * This permission is used to designate refreshing one or more entities from the * catalog. - * @public + * @alpha */ export const catalogEntityRefreshPermission: Permission = { name: 'catalog.entity.refresh', @@ -84,7 +85,7 @@ export const catalogEntityRefreshPermission: Permission = { * * If this permission is not authorized, it will appear that the location does * not exist in the catalog — both in the frontend and in API responses. - * @public + * @alpha */ export const catalogLocationReadPermission: Permission = { name: 'catalog.location.read', @@ -96,7 +97,7 @@ export const catalogLocationReadPermission: Permission = { /** * This permission is used to designate actions that involve creating catalog * locations. - * @public + * @alpha */ export const catalogLocationCreatePermission: Permission = { name: 'catalog.location.create', @@ -108,7 +109,7 @@ export const catalogLocationCreatePermission: Permission = { /** * This permission is used to designate actions that involve deleting locations * from the catalog. - * @public + * @alpha */ export const catalogLocationDeletePermission: Permission = { name: 'catalog.location.delete', diff --git a/plugins/catalog-common/src/search/CatalogEntityDocument.ts b/plugins/catalog-common/src/search/CatalogEntityDocument.ts new file mode 100644 index 0000000000..834dde2568 --- /dev/null +++ b/plugins/catalog-common/src/search/CatalogEntityDocument.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2022 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 { IndexableDocument } from '@backstage/search-common'; + +/** + * The Document format for an Entity in the Catalog for search + * + * @public + */ +export interface CatalogEntityDocument extends IndexableDocument { + /** @deprecated `componentType` is being renamed to `type`. During the transition both of these fields should be set to the same value, in order to avoid issues with indexing. */ + componentType: string; + type: string; + namespace: string; + kind: string; + lifecycle: string; + owner: string; +} diff --git a/plugins/catalog-common/src/search/index.ts b/plugins/catalog-common/src/search/index.ts new file mode 100644 index 0000000000..82e713aba0 --- /dev/null +++ b/plugins/catalog-common/src/search/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 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. + */ +export type { CatalogEntityDocument } from './CatalogEntityDocument'; diff --git a/plugins/catalog-graph/.eslintrc.js b/plugins/catalog-graph/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/catalog-graph/.eslintrc.js +++ b/plugins/catalog-graph/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index 5ce044aa14..84006ba4de 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-catalog-graph +## 0.2.14-next.0 + +### Patch Changes + +- bf95bb806c: Remove usages of now-removed `CatalogApi.getEntityByName` +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/catalog-client@0.9.0-next.0 + +## 0.2.13 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.2.12 ### Patch Changes diff --git a/plugins/catalog-graph/api-report.md b/plugins/catalog-graph/api-report.md index 515a578d3c..82db1dac3b 100644 --- a/plugins/catalog-graph/api-report.md +++ b/plugins/catalog-graph/api-report.md @@ -6,8 +6,8 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { DependencyGraphTypes } from '@backstage/core-components'; -import { EntityName } from '@backstage/catalog-model'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; import { MouseEvent as MouseEvent_2 } from 'react'; @@ -129,7 +129,7 @@ export const EntityRelationsGraph: ({ className, zoom, }: { - rootEntityNames: EntityName | EntityName[]; + rootEntityNames: CompoundEntityRef | CompoundEntityRef[]; maxDepth?: number | undefined; unidirectional?: boolean | undefined; mergeRelations?: boolean | undefined; diff --git a/plugins/catalog-graph/dev/index.tsx b/plugins/catalog-graph/dev/index.tsx index 62d5eccef3..7d772702a1 100644 --- a/plugins/catalog-graph/dev/index.tsx +++ b/plugins/catalog-graph/dev/index.tsx @@ -16,7 +16,7 @@ import { GetEntitiesResponse } from '@backstage/catalog-client'; import { Entity, - EntityName, + CompoundEntityRef, DEFAULT_NAMESPACE, RELATION_API_CONSUMED_BY, RELATION_API_PROVIDED_BY, @@ -139,8 +139,12 @@ createDevApp() deps: {}, factory() { return { - async getEntityByName(name: EntityName): Promise { - return entities[stringifyEntityRef(name)]; + async getEntityByRef( + ref: string | CompoundEntityRef, + ): Promise { + return entities[ + typeof ref === 'string' ? ref : stringifyEntityRef(ref) + ]; }, async getEntities(): Promise { return { items: Object.values(entities) }; diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 0d85123646..d3b65d1151 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.2.12", + "version": "0.2.14-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,11 +24,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -45,11 +45,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/plugin-catalog": "^0.10.0-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index 7cebdee692..9fbe110811 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -57,7 +57,7 @@ describe('', () => { }; catalog = { getEntities: jest.fn(), - getEntityByName: jest.fn(async _ => ({ ...entity, relations: [] })), + getEntityByRef: jest.fn(async _ => ({ ...entity, relations: [] })), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), getLocationByRef: jest.fn(), @@ -88,7 +88,7 @@ describe('', () => { expect(await findByText('b:d/c')).toBeInTheDocument(); expect(await findAllByTestId('node')).toHaveLength(1); - expect(catalog.getEntityByName).toBeCalledTimes(1); + expect(catalog.getEntityByRef).toBeCalledTimes(1); }); test('renders with custom title', async () => { diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index b2dfd9f9b2..4a21418107 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -14,14 +14,14 @@ * limitations under the License. */ import { - getEntityName, + getCompoundEntityRef, parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; import { InfoCard, InfoCardVariants } from '@backstage/core-components'; import { useAnalytics, useRouteRef } from '@backstage/core-plugin-api'; import { - formatEntityRefTitle, + humanizeEntityRef, useEntity, entityRouteRef, } from '@backstage/plugin-catalog-react'; @@ -77,7 +77,7 @@ export const CatalogGraphCard = ({ zoom?: 'enabled' | 'disabled' | 'enable-on-click'; }) => { const { entity } = useEntity(); - const entityName = getEntityName(entity); + const entityName = getCompoundEntityRef(entity); const catalogEntityRoute = useRouteRef(entityRouteRef); const catalogGraphRoute = useRouteRef(catalogGraphRouteRef); const navigate = useNavigate(); @@ -94,7 +94,7 @@ export const CatalogGraphCard = ({ }); analytics.captureEvent( 'click', - node.title ?? formatEntityRefTitle(nodeEntityName), + node.title ?? humanizeEntityRef(nodeEntityName), { attributes: { to: path } }, ); navigate(path); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx index 2f686f8f62..9453d5a000 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx @@ -88,7 +88,9 @@ describe('', () => { }; catalog = { getEntities: jest.fn(), - getEntityByName: jest.fn(async n => (n.name === 'e' ? entityE : entityC)), + getEntityByRef: jest.fn(async (n: any) => + n === 'b:d/e' ? entityE : entityC, + ), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), getLocationByRef: jest.fn(), @@ -128,7 +130,7 @@ describe('', () => { expect(await findByText('b:d/c')).toBeInTheDocument(); expect(await findByText('b:d/e')).toBeInTheDocument(); expect(await findAllByTestId('node')).toHaveLength(2); - expect(catalog.getEntityByName).toBeCalledTimes(2); + expect(catalog.getEntityByRef).toBeCalledTimes(2); }); test('should toggle filters', async () => { diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx index 2ae7a99dd8..15d0cd139b 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx @@ -24,7 +24,7 @@ import { import { useAnalytics, useRouteRef } from '@backstage/core-plugin-api'; import { entityRouteRef, - formatEntityRefTitle, + humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { Grid, makeStyles, Paper, Typography } from '@material-ui/core'; import FilterListIcon from '@material-ui/icons/FilterList'; @@ -149,14 +149,14 @@ export const CatalogGraphPage = ({ analytics.captureEvent( 'click', - node.title ?? formatEntityRefTitle(nodeEntityName), + node.title ?? humanizeEntityRef(nodeEntityName), { attributes: { to: path } }, ); navigate(path); } else { analytics.captureEvent( 'click', - node.title ?? formatEntityRefTitle(nodeEntityName), + node.title ?? humanizeEntityRef(nodeEntityName), ); setRootEntityNames([nodeEntityName]); } @@ -168,7 +168,7 @@ export const CatalogGraphPage = ({

formatEntityRefTitle(e)).join(', ')} + subtitle={rootEntityNames.map(e => humanizeEntityRef(e)).join(', ')} /> >; + rootEntityNames: CompoundEntityRef[]; + setRootEntityNames: Dispatch>; maxDepth: number; setMaxDepth: Dispatch>; selectedRelations: string[] | undefined; @@ -82,11 +82,12 @@ export function useCatalogGraphPage({ ); // Initial state - const [rootEntityNames, setRootEntityNames] = useState(() => - (Array.isArray(query.rootEntityRefs) - ? query.rootEntityRefs - : initialState?.rootEntityRefs ?? [] - ).map(r => parseEntityRef(r)), + const [rootEntityNames, setRootEntityNames] = useState( + () => + (Array.isArray(query.rootEntityRefs) + ? query.rootEntityRefs + : initialState?.rootEntityRefs ?? [] + ).map(r => parseEntityRef(r)), ); const [maxDepth, setMaxDepth] = useState(() => typeof query.maxDepth === 'string' diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx index 9cd752a5d7..ae541fa997 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { DependencyGraphTypes } from '@backstage/core-components'; -import { formatEntityRefTitle } from '@backstage/plugin-catalog-react'; +import { humanizeEntityRef } from '@backstage/plugin-catalog-react'; import { BackstageTheme } from '@backstage/theme'; import { makeStyles } from '@material-ui/core/styles'; import classNames from 'classnames'; @@ -95,7 +95,7 @@ export function CustomNode({ const displayTitle = title ?? (kind && name && namespace - ? formatEntityRefTitle({ kind, name, namespace }) + ? humanizeEntityRef({ kind, name, namespace }) : id); return ( diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx index 36dc816444..e8c9239739 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity, RELATION_HAS_PART, RELATION_OWNED_BY, RELATION_OWNER_OF, RELATION_PART_OF, - stringifyEntityRef, } from '@backstage/catalog-model'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; @@ -155,7 +155,7 @@ describe('', () => { }; catalog = { getEntities: jest.fn(), - getEntityByName: jest.fn(async n => entities[stringifyEntityRef(n)]), + getEntityByRef: jest.fn(async n => entities[n as string]), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), getLocationByRef: jest.fn(), @@ -178,7 +178,7 @@ describe('', () => { }); test('renders a single node without exploding', async () => { - catalog.getEntityByName.mockResolvedValue({ + catalog.getEntityByRef.mockResolvedValue({ apiVersion: 'a', kind: 'b', metadata: { @@ -198,11 +198,11 @@ describe('', () => { expect(await findByText('b:d/c')).toBeInTheDocument(); expect(await findAllByTestId('node')).toHaveLength(1); - expect(catalog.getEntityByName).toBeCalledTimes(1); + expect(catalog.getEntityByRef).toBeCalledTimes(1); }); test('renders a progress indicator while loading', async () => { - catalog.getEntityByName.mockImplementation(() => new Promise(() => {})); + catalog.getEntityByRef.mockImplementation(() => new Promise(() => {})); const { findByRole } = await renderInTestApp( @@ -213,12 +213,12 @@ describe('', () => { ); expect(await findByRole('progressbar')).toBeInTheDocument(); - expect(catalog.getEntityByName).toBeCalledTimes(1); + expect(catalog.getEntityByRef).toBeCalledTimes(1); }); test('does not explode if an entity is missing', async () => { - catalog.getEntityByName.mockImplementation(async n => { - if (n.name === 'c') { + catalog.getEntityByRef.mockImplementation(async (n: any) => { + if (n === 'b:d/c') { return { apiVersion: 'a', kind: 'b', @@ -253,7 +253,7 @@ describe('', () => { expect(await findByText('b:d/c')).toBeInTheDocument(); expect(await findAllByTestId('node')).toHaveLength(1); - expect(catalog.getEntityByName).toBeCalledTimes(2); + expect(catalog.getEntityByRef).toBeCalledTimes(2); }); test('renders at max depth of one', async () => { @@ -276,7 +276,7 @@ describe('', () => { expect(await findAllByText('hasPart')).toHaveLength(1); expect(await findAllByTestId('label')).toHaveLength(2); - expect(catalog.getEntityByName).toBeCalledTimes(3); + expect(catalog.getEntityByRef).toBeCalledTimes(3); }); test('renders simplied graph at full depth', async () => { @@ -301,7 +301,7 @@ describe('', () => { expect(await findAllByText('hasPart')).toHaveLength(2); expect(await findAllByTestId('label')).toHaveLength(3); - expect(catalog.getEntityByName).toBeCalledTimes(4); + expect(catalog.getEntityByRef).toBeCalledTimes(4); }); test('renders full graph at full depth', async () => { @@ -328,7 +328,7 @@ describe('', () => { expect(await findAllByText('partOf')).toHaveLength(2); expect(await findAllByTestId('label')).toHaveLength(8); - expect(catalog.getEntityByName).toBeCalledTimes(4); + expect(catalog.getEntityByRef).toBeCalledTimes(4); }); test('renders full graph at full depth with merged relations', async () => { @@ -353,7 +353,7 @@ describe('', () => { expect(await findAllByText('hasPart')).toHaveLength(2); expect(await findAllByTestId('label')).toHaveLength(4); - expect(catalog.getEntityByName).toBeCalledTimes(4); + expect(catalog.getEntityByRef).toBeCalledTimes(4); }); test('renders a graph with multiple root nodes', async () => { @@ -379,7 +379,7 @@ describe('', () => { expect(await findAllByText('partOf')).toHaveLength(2); expect(await findAllByTestId('label')).toHaveLength(3); - expect(catalog.getEntityByName).toBeCalledTimes(4); + expect(catalog.getEntityByRef).toBeCalledTimes(4); }); test('renders a graph with filtered kinds and relations', async () => { @@ -401,7 +401,7 @@ describe('', () => { expect(await findAllByText('ownerOf')).toHaveLength(1); expect(await findAllByTestId('label')).toHaveLength(1); - expect(catalog.getEntityByName).toBeCalledTimes(2); + expect(catalog.getEntityByRef).toBeCalledTimes(2); }); test('handle clicks on a node', async () => { diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index 76cd44394a..c5c83a54cd 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -13,7 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityName, stringifyEntityRef } from '@backstage/catalog-model'; +import { + CompoundEntityRef, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { DependencyGraph, DependencyGraphTypes, @@ -77,7 +80,7 @@ export const EntityRelationsGraph = ({ className, zoom = 'enabled', }: { - rootEntityNames: EntityName | EntityName[]; + rootEntityNames: CompoundEntityRef | CompoundEntityRef[]; maxDepth?: number; unidirectional?: boolean; mergeRelations?: boolean; diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts index a5dc82eab9..9956137287 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts @@ -29,7 +29,7 @@ describe('useEntityStore', () => { beforeEach(() => { catalogApi = { getEntities: jest.fn(), - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), getLocationByRef: jest.fn(), @@ -64,7 +64,7 @@ describe('useEntityStore', () => { }, }; - catalogApi.getEntityByName.mockResolvedValue(entity); + catalogApi.getEntityByRef.mockResolvedValue(entity); const { result, waitFor } = renderHook(() => useEntityStore()); @@ -84,7 +84,7 @@ describe('useEntityStore', () => { test('handles request failures', async () => { const err = new Error('Hello World'); - catalogApi.getEntityByName.mockRejectedValue(err); + catalogApi.getEntityByRef.mockRejectedValue(err); const { result, waitFor } = renderHook(() => useEntityStore()); @@ -101,7 +101,7 @@ describe('useEntityStore', () => { }); test('handles loading', async () => { - catalogApi.getEntityByName.mockReturnValue(new Promise(() => {})); + catalogApi.getEntityByRef.mockReturnValue(new Promise(() => {})); const { result } = renderHook(() => useEntityStore()); @@ -133,7 +133,7 @@ describe('useEntityStore', () => { }, }; - catalogApi.getEntityByName.mockResolvedValue(entity1); + catalogApi.getEntityByRef.mockResolvedValue(entity1); const { result, waitFor } = renderHook(() => useEntityStore()); @@ -150,7 +150,7 @@ describe('useEntityStore', () => { }); }); - catalogApi.getEntityByName.mockResolvedValue(entity2); + catalogApi.getEntityByRef.mockResolvedValue(entity2); act(() => { result.current.requestEntities([ @@ -188,7 +188,7 @@ describe('useEntityStore', () => { }, }; - catalogApi.getEntityByName.mockResolvedValue(entity1); + catalogApi.getEntityByRef.mockResolvedValue(entity1); const { result, waitFor } = renderHook(() => useEntityStore()); @@ -205,7 +205,7 @@ describe('useEntityStore', () => { }); }); - catalogApi.getEntityByName.mockResolvedValue(entity2); + catalogApi.getEntityByRef.mockResolvedValue(entity2); act(() => { result.current.requestEntities(['kind:namespace/name2']); @@ -233,6 +233,6 @@ describe('useEntityStore', () => { }); }); - expect(catalogApi.getEntityByName).toBeCalledTimes(2); + expect(catalogApi.getEntityByRef).toBeCalledTimes(2); }); }); diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts index b5ec3bef19..843b308281 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, parseEntityRef } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import limiterFactory from 'p-limit'; @@ -73,9 +73,7 @@ export function useEntityStore(): { return; } - const promise = catalogClient.getEntityByName( - parseEntityRef(entityRef), - ); + const promise = catalogClient.getEntityByRef(entityRef); outstandingEntities.set(entityRef, promise); diff --git a/plugins/catalog-graphql/.eslintrc.js b/plugins/catalog-graphql/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/catalog-graphql/.eslintrc.js +++ b/plugins/catalog-graphql/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index cd1f255e23..542b272a02 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-graphql +## 0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.13.0-next.0 + +## 0.3.5 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + ## 0.3.4 ### Patch Changes diff --git a/plugins/catalog-graphql/api-report.md b/plugins/catalog-graphql/api-report.md index 019f9c279f..e14fd988d0 100644 --- a/plugins/catalog-graphql/api-report.md +++ b/plugins/catalog-graphql/api-report.md @@ -4,7 +4,7 @@ ```ts import { Config } from '@backstage/config'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { Module } from 'graphql-modules'; // Warning: (ae-missing-release-tag) "createModule" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -19,6 +19,6 @@ export interface ModuleOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; } ``` diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index 6ecf7eb512..7afbab0698 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-graphql", "description": "An experimental Backstage catalog GraphQL module", - "version": "0.3.4", + "version": "0.3.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,7 +34,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/types": "^0.1.3", "graphql-modules": "^2.0.0", @@ -46,8 +46,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/test-utils": "^0.3.0", "@graphql-codegen/cli": "^2.3.1", "@graphql-codegen/typescript": "^2.4.2", "@graphql-codegen/typescript-resolvers": "^2.4.3", diff --git a/plugins/catalog-import/.eslintrc.js b/plugins/catalog-import/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/catalog-import/.eslintrc.js +++ b/plugins/catalog-import/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index b55f8b34fa..db8d59eb51 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-catalog-import +## 0.8.5-next.0 + +### Patch Changes + +- bf95bb806c: Remove usages of now-removed `CatalogApi.getEntityByName` +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/catalog-client@0.9.0-next.0 + - @backstage/integration-react@0.1.25-next.0 + +## 0.8.4 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/integration-react@0.1.24 + ## 0.8.3 ### Patch Changes diff --git a/plugins/catalog-import/api-report.md b/plugins/catalog-import/api-report.md index 164d831c96..5a9e8f03fd 100644 --- a/plugins/catalog-import/api-report.md +++ b/plugins/catalog-import/api-report.md @@ -8,11 +8,11 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { ConfigApi } from '@backstage/core-plugin-api'; import { Controller } from 'react-hook-form'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; import { FieldErrors } from 'react-hook-form'; import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; @@ -33,7 +33,7 @@ export type AnalyzeResult = locations: Array<{ target: string; exists?: boolean; - entities: EntityName[]; + entities: CompoundEntityRef[]; }>; } | { @@ -165,7 +165,7 @@ export interface EntityListComponentProps { // (undocumented) locations: Array<{ target: string; - entities: (Entity | EntityName)[]; + entities: (Entity | CompoundEntityRef)[]; }>; // (undocumented) onItemClick?: (target: string) => void; @@ -246,7 +246,7 @@ export type PrepareResult = locations: Array<{ exists?: boolean; target: string; - entities: EntityName[]; + entities: CompoundEntityRef[]; }>; } | { @@ -258,7 +258,7 @@ export type PrepareResult = }; locations: Array<{ target: string; - entities: EntityName[]; + entities: CompoundEntityRef[]; }>; }; diff --git a/plugins/catalog-import/dev/index.tsx b/plugins/catalog-import/dev/index.tsx index a0cf034782..e2e5e1d22c 100644 --- a/plugins/catalog-import/dev/index.tsx +++ b/plugins/catalog-import/dev/index.tsx @@ -15,7 +15,7 @@ */ import { CatalogApi } from '@backstage/catalog-client'; -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { createDevApp } from '@backstage/dev-utils'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Grid, ListItem, ListItemIcon, ListItemText } from '@material-ui/core'; @@ -32,7 +32,7 @@ import { import { ImportPage } from '../src/components/ImportPage'; import { Content, Header, InfoCard, Page } from '@backstage/core-components'; -const getEntityNames = (url: string): EntityName[] => [ +const getEntityNames = (url: string): CompoundEntityRef[] => [ { kind: 'Component', namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 800b747e8a..566602f5ba 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-import", "description": "A Backstage plugin the helps you import entities into your catalog", - "version": "0.8.3", + "version": "0.8.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,15 +34,15 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", "@backstage/config": "^0.1.15", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/integration": "^0.8.0", + "@backstage/integration-react": "^0.1.25-next.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -60,10 +60,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts index 0923f4ac98..3cfd53470a 100644 --- a/plugins/catalog-import/src/api/CatalogImportApi.ts +++ b/plugins/catalog-import/src/api/CatalogImportApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { createApiRef } from '@backstage/core-plugin-api'; import { PartialEntity } from '../types'; @@ -38,7 +38,7 @@ export type AnalyzeResult = locations: Array<{ target: string; exists?: boolean; - entities: EntityName[]; + entities: CompoundEntityRef[]; }>; } | { diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index f689c0a883..5a65738d61 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -93,7 +93,7 @@ describe('CatalogImportClient', () => { getEntities: jest.fn(), addLocation: jest.fn(), removeLocationById: jest.fn(), - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeEntityByUid: jest.fn(), diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index b856a4e359..c12e391ec9 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -15,7 +15,7 @@ */ import { CatalogApi } from '@backstage/catalog-client'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { ConfigApi, DiscoveryApi, @@ -212,7 +212,7 @@ the component will become available.\n\nFor more information, read an \ }): Promise< Array<{ target: string; - entities: EntityName[]; + entities: CompoundEntityRef[]; }> > { const { url, owner, repo, githubIntegrationConfig } = options; diff --git a/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx index 2c59eec18f..47f0139e5e 100644 --- a/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx +++ b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { useApp } from '@backstage/core-plugin-api'; import { EntityRefLink, - formatEntityRefTitle, + humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { Collapse, @@ -41,9 +41,9 @@ const useStyles = makeStyles(theme => ({ }, })); -function sortEntities(entities: Array) { +function sortEntities(entities: Array) { return entities.sort((a, b) => - formatEntityRefTitle(a).localeCompare(formatEntityRefTitle(b)), + humanizeEntityRef(a).localeCompare(humanizeEntityRef(b)), ); } @@ -53,7 +53,10 @@ function sortEntities(entities: Array) { * @public */ export interface EntityListComponentProps { - locations: Array<{ target: string; entities: (Entity | EntityName)[] }>; + locations: Array<{ + target: string; + entities: (Entity | CompoundEntityRef)[]; + }>; locationListItemIcon: (target: string) => React.ReactElement; collapsed?: boolean; firstListItem?: React.ReactElement; @@ -130,7 +133,7 @@ export const EntityListComponent = (props: EntityListComponentProps) => { ) ?? WorkIcon; return ( { - + ); })} diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index fc9b35136a..ea2bee9425 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -38,7 +38,7 @@ describe('', () => { const catalogApi: jest.Mocked = { getEntities: jest.fn(), addLocation: jest.fn(), - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeLocationById: jest.fn(), diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx index 36e8c33a5c..1cbdfce355 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx @@ -19,7 +19,7 @@ import { errorApiRef, useApi } from '@backstage/core-plugin-api'; import { assertError } from '@backstage/errors'; import { catalogApiRef, - formatEntityRefTitle, + humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { Box, FormHelperText, Grid, Typography } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; @@ -139,7 +139,7 @@ export const StepPrepareCreatePullRequest = ( }); return groupEntities.items - .map(e => formatEntityRefTitle(e, { defaultKind: 'group' })) + .map(e => humanizeEntityRef(e, { defaultKind: 'group' })) .sort(); }); diff --git a/plugins/catalog-import/src/components/useImportState.test.tsx b/plugins/catalog-import/src/components/useImportState.test.tsx index 1a01c61449..155adb8290 100644 --- a/plugins/catalog-import/src/components/useImportState.test.tsx +++ b/plugins/catalog-import/src/components/useImportState.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { cleanup } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react-hooks'; import { AnalyzeResult } from '../api'; @@ -37,7 +37,7 @@ describe('useImportState', () => { locations: [ { target: 'https://0', - entities: [] as EntityName[], + entities: [] as CompoundEntityRef[], }, ], }; diff --git a/plugins/catalog-import/src/components/useImportState.ts b/plugins/catalog-import/src/components/useImportState.ts index 0cb6b59917..965f855f30 100644 --- a/plugins/catalog-import/src/components/useImportState.ts +++ b/plugins/catalog-import/src/components/useImportState.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { useReducer } from 'react'; import { AnalyzeResult } from '../api'; @@ -43,7 +43,7 @@ export type PrepareResult = locations: Array<{ exists?: boolean; target: string; - entities: EntityName[]; + entities: CompoundEntityRef[]; }>; } | { @@ -55,7 +55,7 @@ export type PrepareResult = }; locations: Array<{ target: string; - entities: EntityName[]; + entities: CompoundEntityRef[]; }>; }; diff --git a/plugins/catalog-react/.eslintrc.js b/plugins/catalog-react/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/catalog-react/.eslintrc.js +++ b/plugins/catalog-react/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index ac611032e5..3bffeeef99 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,84 @@ # @backstage/plugin-catalog-react +## 0.9.0-next.0 + +### Minor Changes + +- b0af81726d: **BREAKING**: Removed `reduceCatalogFilters` and `reduceEntityFilters` due to low external utility value. +- 7ffb2c73c9: **BREAKING**: Removed the deprecated `loadCatalogOwnerRefs` function. Usages of this function can be directly replaced with `ownershipEntityRefs` from `identityApi.getBackstageIdentity()`. + + This also affects the `useEntityOwnership` hook in that it no longer uses `loadCatalogOwnerRefs`, meaning it will no longer load in additional relations and instead only rely on the `ownershipEntityRefs` from the `IdentityApi`. + +- dd88d1e3ac: **BREAKING**: Removed `useEntityFromUrl`. +- 9844d4d2bd: **BREAKING**: Removed `useEntityCompoundName`, use `useRouteRefParams(entityRouteRef)` instead. +- 2b8c986ce0: **BREAKING**: Removed `useEntityListProvider` use `useEntityList` instead. +- f3a7a9de6d: **BREAKING**: Removed `useOwnedEntities` and moved its usage internally to the scaffolder-backend where it's used. + + **BREAKING**: Removed `EntityTypeReturn` type which is now inlined. + +- cf1ff5b438: **BREAKING**: Removed the `useEntityKinds` hook, use `catalogApi.getEntityFacets({ facets: ['kind'] })` instead. +- fc6290a76d: **BREAKING**: Removed the deprecated `useOwnUser` hook. Existing usage can be replaced with `identityApi.getBackstageIdentity()`, followed by a call to `catalogClient.getEntityByRef(identity.userEntityRef)`. + +### Patch Changes + +- b1aacbf96a: Applied the fix for the `/alpha` entry point resolution that was part of the `v0.70.1` release of Backstage. +- 2986f8e09d: Fixed EntityOwnerPicker and OwnershipCard url filter issue with more than 21 owners +- f3a7a9de6d: Internalized usage of `useOwnedEntities` hook. +- Updated dependencies + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/catalog-client@0.9.0-next.0 + +## 0.8.1 + +### Patch Changes + +- Fixed runtime resolution of the `/alpha` entry point. +- Updated dependencies + - @backstage/catalog-model@0.12.1 + +## 0.8.0 + +### Minor Changes + +- da79aac2a6: Removed some previously deprecated `routeRefs` as follows: + + - **BREAKING**: Removed `entityRoute` in favor of `entityRouteRef`. + - **BREAKING**: Removed the previously deprecated `rootRoute` and `catalogRouteRef`. If you want to refer to the catalog index page from a public plugin you now need to use an `ExternalRouteRef` instead. For private plugins it is possible to take the shortcut of referring directly to `catalogPlugin.routes.indexPage` instead. + +- e26fd1c7ab: Marked `useEntityPermission` as alpha since the underlying permission framework is under active development. +- 2de1d82bd1: Removing the `EntityName` path for the `useEntityOwnership` as it has never worked correctly. Please pass in an entire `Entity` instead. + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- f590d1681b: Deprecated `favoriteEntityTooltip` and `favoriteEntityIcon` since the utility value is very low. +- 72431d7bed: - **BREAKING**: The `isOwnerOf` function has been marked as `@alpha` and is now only available via the `@backstage/plugin-catalog-react/alpha` import. The limitations of this function with regards to only supporting direct relations have also been documented. +- 03ec06bf7f: **BREAKING**: Moved **DefaultStarredEntitiesApi** to `@backstage/plugin-catalog`. If you were using this in tests, you can use the new `MockStarredEntitiesApi` from `@backstage/plugin-catalog-react` instead. + + Fixed a risky behavior where `DefaultStarredEntitiesApi` forwarded values to observers that were later mutated. + + Removed the `isStarred` method from `DefaultStarredEntitiesApi`, as it is not part of the `StarredEntitiesApi`. + +- 44403296e7: Added the following deprecations to the `catalog-react` package: + + - **DEPRECATION**: `useEntity` will now warn if the entity has not yet been loaded, and will soon throw errors instead. If you're using the default implementation of `EntityLayout` and `EntitySwitch` then these components will ensure that there is an entity loaded before rendering children. If you're implementing your own `EntityLayout` or `EntitySwitch` or something that operates outside or adjacent to them, then use `useAsyncEntity`. + + - **DEPRECATION**: the `loading`, `error` and `refresh` properties that are returned from `useEntity` have been deprecated, and are available on `useAsyncEntity` instead. + +- 8f0e8e039b: Deprecated `getEntityMetadataEditUrl` and `getEntityMetadataViewUrl` as these just return one annotation from the entity passed in. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- bb2bb36651: Updated usage of `StorageApi` to use `snapshot` method instead of `get` +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/core-components@0.9.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-permission-react@0.3.3 + ## 0.7.0 ### Minor Changes diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index e78ec75daa..4912c857cf 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -6,14 +6,12 @@ /// import { ApiRef } from '@backstage/core-plugin-api'; -import { AsyncState } from 'react-use/lib/useAsync'; import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { CatalogApi } from '@backstage/catalog-client'; import { ComponentEntity } from '@backstage/catalog-model'; import { ComponentProps } from 'react'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; -import { GetEntitiesResponse } from '@backstage/catalog-client'; import { IconButton } from '@material-ui/core'; import { LinkProps } from '@backstage/core-components'; import { Observable } from '@backstage/types'; @@ -24,11 +22,9 @@ import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { StorageApi } from '@backstage/core-plugin-api'; import { StyleRules } from '@material-ui/core/styles/withStyles'; import { SystemEntity } from '@backstage/catalog-model'; import { TableColumn } from '@backstage/core-components'; -import { UserEntity } from '@backstage/catalog-model'; // @public export const AsyncEntityProvider: ({ @@ -96,9 +92,6 @@ export type CatalogReactUserListPickerClassKey = | 'menuItem' | 'groupWrapper'; -// @public @deprecated (undocumented) -export const catalogRouteRef: RouteRef; - // @public (undocumented) export const columnFactories: Readonly<{ createEntityRefColumn(options: { @@ -138,17 +131,6 @@ export type DefaultEntityFilters = { text?: EntityTextFilter; }; -// @public -export class DefaultStarredEntitiesApi implements StarredEntitiesApi { - constructor(opts: { storageApi: StorageApi }); - // (undocumented) - isStarred(entityRef: string): boolean; - // (undocumented) - starredEntitie$(): Observable>; - // (undocumented) - toggleStarred(entityRef: string): Promise; -} - // @public (undocumented) export type EntityFilter = { getCatalogFilters?: () => Record< @@ -225,8 +207,8 @@ export const EntityListProvider: ({ }: PropsWithChildren<{}>) => JSX.Element; // @public (undocumented) -export type EntityLoadingStatus = { - entity?: Entity; +export type EntityLoadingStatus = { + entity?: TEntity; loading: boolean; error?: Error; refresh?: VoidFunction; @@ -265,7 +247,7 @@ export const EntityRefLink: (props: EntityRefLinkProps) => JSX.Element; // @public export type EntityRefLinkProps = { - entityRef: Entity | EntityName | string; + entityRef: Entity | CompoundEntityRef | string; defaultKind?: string; title?: string; children?: React_2.ReactNode; @@ -280,17 +262,10 @@ export const EntityRefLinks: ({ // @public export type EntityRefLinksProps = { - entityRefs: (Entity | EntityName)[]; + entityRefs: (Entity | CompoundEntityRef)[]; defaultKind?: string; } & Omit; -// @public @deprecated (undocumented) -export const entityRoute: RouteRef<{ - name: string; - kind: string; - namespace: string; -}>; - // @public export function entityRouteParams(entity: Entity): { readonly kind: string; @@ -410,19 +385,10 @@ export interface EntityTypePickerProps { initialFilter?: string; } -// @public @deprecated (undocumented) -export type EntityTypeReturn = { - loading: boolean; - error?: Error; - availableTypes: string[]; - selectedTypes: string[]; - setSelectedTypes: (types: string[]) => void; -}; - // @public export const FavoriteEntity: (props: FavoriteEntityProps) => JSX.Element; -// @public (undocumented) +// @public @deprecated (undocumented) export const favoriteEntityIcon: (isStarred: boolean) => JSX.Element; // @public (undocumented) @@ -430,18 +396,13 @@ export type FavoriteEntityProps = ComponentProps & { entity: Entity; }; -// @public (undocumented) +// @public @deprecated (undocumented) export const favoriteEntityTooltip: ( isStarred: boolean, ) => 'Remove from favorites' | 'Add to favorites'; -// @public (undocumented) -export function formatEntityRefTitle( - entityRef: Entity | EntityName, - opts?: { - defaultKind?: string; - }, -): string; +// @public @deprecated (undocumented) +export const formatEntityRefTitle: typeof humanizeEntityRef; // @public @deprecated (undocumented) export function getEntityMetadataEditUrl(entity: Entity): string | undefined; @@ -456,7 +417,7 @@ export function getEntityRelations( filter?: { kind: string; }, -): EntityName[]; +): CompoundEntityRef[]; // @public (undocumented) export function getEntitySourceLocation( @@ -464,6 +425,14 @@ export function getEntitySourceLocation( scmIntegrationsApi: ScmIntegrationRegistry, ): EntitySourceLocation | undefined; +// @public (undocumented) +export function humanizeEntityRef( + entityRef: Entity | CompoundEntityRef, + opts?: { + defaultKind?: string; + }, +): string; + // @public export function InspectEntityDialog(props: { open: boolean; @@ -471,14 +440,8 @@ export function InspectEntityDialog(props: { onClose: () => void; }): JSX.Element | null; -// @public -export function isOwnerOf(owner: Entity, owned: Entity): boolean; - -// @public @deprecated -export function loadCatalogOwnerRefs( - catalogApi: CatalogApi, - identityOwnerRefs: string[], -): Promise; +// @alpha +export function isOwnerOf(owner: Entity, entity: Entity): boolean; // @public (undocumented) export const MockEntityListContextProvider: ({ @@ -488,18 +451,13 @@ export const MockEntityListContextProvider: ({ value?: Partial> | undefined; }>) => JSX.Element; -// @public @deprecated (undocumented) -export function reduceCatalogFilters( - filters: EntityFilter[], -): Record; - -// @public @deprecated (undocumented) -export function reduceEntityFilters( - filters: EntityFilter[], -): (entity: Entity) => boolean; - -// @public @deprecated (undocumented) -export const rootRoute: RouteRef; +// @public +export class MockStarredEntitiesApi implements StarredEntitiesApi { + // (undocumented) + starredEntitie$(): Observable>; + // (undocumented) + toggleStarred(entityRef: string): Promise; +} // @public export interface StarredEntitiesApi { @@ -524,28 +482,16 @@ export type UnregisterEntityDialogProps = { }; // @public -export function useEntity(): { - entity: T; +export function useAsyncEntity< + TEntity extends Entity = Entity, +>(): EntityLoadingStatus; + +// @public +export function useEntity(): { + entity: TEntity; loading: boolean; - error: Error | undefined; - refresh: VoidFunction | undefined; -}; - -// @public @deprecated -export const useEntityCompoundName: () => { - kind: string; - namespace: string; - name: string; -}; - -// @public @deprecated (undocumented) -export const useEntityFromUrl: () => EntityLoadingStatus; - -// @public @deprecated -export function useEntityKinds(): { - error: Error | undefined; - loading: boolean; - kinds: string[] | undefined; + error?: Error; + refresh?: VoidFunction; }; // @public @@ -553,18 +499,13 @@ export function useEntityList< EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, >(): EntityListContextProps; -// @public @deprecated -export function useEntityListProvider< - EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, ->(): EntityListContextProps; - // @public export function useEntityOwnership(): { loading: boolean; - isOwnedEntity: (entity: Entity | EntityName) => boolean; + isOwnedEntity: (entity: Entity) => boolean; }; -// @public +// @alpha export function useEntityPermission(permission: Permission): { loading: boolean; allowed: boolean; @@ -580,15 +521,6 @@ export function useEntityTypeFilter(): { setSelectedTypes: (types: string[]) => void; }; -// @public @deprecated -export function useOwnedEntities(allowedKinds?: string[]): { - loading: boolean; - ownedEntities: GetEntitiesResponse | undefined; -}; - -// @public @deprecated -export function useOwnUser(): AsyncState; - // @public (undocumented) export function useRelatedEntities( entity: Entity, @@ -636,12 +568,18 @@ export type UserListPickerProps = { // @public (undocumented) export function useStarredEntities(): { starredEntities: Set; - toggleStarredEntity: (entityOrRef: Entity | EntityName | string) => void; - isStarredEntity: (entityOrRef: Entity | EntityName | string) => boolean; + toggleStarredEntity: ( + entityOrRef: Entity | CompoundEntityRef | string, + ) => void; + isStarredEntity: ( + entityOrRef: Entity | CompoundEntityRef | string, + ) => boolean; }; // @public (undocumented) -export function useStarredEntity(entityOrRef: Entity | EntityName | string): { +export function useStarredEntity( + entityOrRef: Entity | CompoundEntityRef | string, +): { toggleStarredEntity: () => void; isStarredEntity: boolean; }; diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index c7e255f973..f7c17d74ac 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "0.7.0", + "version": "0.9.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -9,7 +9,8 @@ "publishConfig": { "access": "public", "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "web-library" @@ -24,7 +25,7 @@ "backstage" ], "scripts": { - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -33,14 +34,14 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-permission-react": "^0.3.2", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-permission-react": "^0.3.3", "@backstage/types": "^0.1.3", "@backstage/version-bridge": "^0.1.2", "@material-ui/core": "^4.12.2", @@ -60,11 +61,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/plugin-catalog-common": "^0.1.4", - "@backstage/plugin-scaffolder-common": "^0.2.2", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/plugin-catalog-common": "^0.2.2-next.0", + "@backstage/plugin-scaffolder-common": "^0.3.0-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -76,6 +77,7 @@ "react-test-renderer": "^16.13.1" }, "files": [ - "dist" + "dist", + "alpha" ] } diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts deleted file mode 100644 index 55b992fadf..0000000000 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { stringifyEntityRef } from '@backstage/catalog-model'; -import { StorageApi } from '@backstage/core-plugin-api'; -import { MockStorageApi } from '@backstage/test-utils'; -import { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi'; -import { performMigrationToTheNewBucket } from './migration'; - -jest.mock('./migration'); - -describe('DefaultStarredEntitiesApi', () => { - let mockStorage: StorageApi; - let starredEntitiesApi: DefaultStarredEntitiesApi; - - const mockEntityRef = stringifyEntityRef({ - apiVersion: '1', - kind: 'Component', - metadata: { - name: 'mock', - }, - }); - - beforeEach(() => { - (performMigrationToTheNewBucket as jest.Mock).mockResolvedValue(undefined); - - mockStorage = MockStorageApi.create(); - starredEntitiesApi = new DefaultStarredEntitiesApi({ - storageApi: mockStorage, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('constructor', () => { - it('should call migration', () => { - expect(performMigrationToTheNewBucket).toBeCalledTimes(1); - }); - }); - - describe('toggleStarred', () => { - it('should star unstarred entity', async () => { - expect(starredEntitiesApi.isStarred(mockEntityRef)).toBe(false); - - await starredEntitiesApi.toggleStarred(mockEntityRef); - - expect(starredEntitiesApi.isStarred(mockEntityRef)).toBe(true); - }); - - it('should unstar starred entity', async () => { - const bucket = mockStorage.forBucket('starredEntities'); - await bucket.set('entityRefs', ['component:default/mock']); - - expect(starredEntitiesApi.isStarred(mockEntityRef)).toBe(true); - - await starredEntitiesApi.toggleStarred(mockEntityRef); - - expect(starredEntitiesApi.isStarred(mockEntityRef)).toBe(false); - }); - }); - - describe('starredEntities$', () => { - const handler = jest.fn(); - - beforeEach(async () => { - await new Promise(resolve => { - starredEntitiesApi.starredEntitie$().subscribe({ - next: (...args) => { - handler(...args); - - if (handler.mock.calls.length >= 2) { - resolve(); - } - }, - }); - - const bucket = mockStorage.forBucket('starredEntities'); - bucket.set('entityRefs', ['component:default/mock']).then(); - }); - }); - - it('should receive updates', async () => { - expect(handler).toBeCalledTimes(2); - expect(handler).toBeCalledWith(new Set()); - expect(handler).toBeCalledWith(new Set(['component:default/mock'])); - }); - }); -}); diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.test.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.test.ts new file mode 100644 index 0000000000..aadecefe98 --- /dev/null +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MockStarredEntitiesApi } from './MockStarredEntitiesApi'; + +describe('MockStarredEntitiesApi', () => { + it('should toggle starred entities', async () => { + const api = new MockStarredEntitiesApi(); + + const updates1 = new Array>(); + const sub1 = api + .starredEntitie$() + .subscribe(entities => updates1.push(entities)); + + api.toggleStarred('k:ns/e1'); + api.toggleStarred('k:ns/e2'); + + await Promise.resolve(); + expect(updates1).toEqual([ + new Set(), + new Set(['k:ns/e1']), + new Set(['k:ns/e1', 'k:ns/e2']), + ]); + + const updates2 = new Array>(); + const sub2 = api + .starredEntitie$() + .subscribe(entities => updates2.push(entities)); + + api.toggleStarred('k:ns/e2'); + sub1.unsubscribe(); + api.toggleStarred('k:ns/e2'); + + await Promise.resolve(); + expect(updates1).toEqual([ + new Set(), + new Set(['k:ns/e1']), + new Set(['k:ns/e1', 'k:ns/e2']), + new Set(['k:ns/e1']), + ]); + expect(updates2).toEqual([ + new Set(['k:ns/e1', 'k:ns/e2']), + new Set(['k:ns/e1']), + new Set(['k:ns/e1', 'k:ns/e2']), + ]); + sub2.unsubscribe(); + }); +}); diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.ts new file mode 100644 index 0000000000..9467153dd9 --- /dev/null +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Observable } from '@backstage/types'; +import ObservableImpl from 'zen-observable'; +import { StarredEntitiesApi } from './StarredEntitiesApi'; + +/** + * An in-memory mock implementation of the StarredEntitiesApi. + * + * @public + */ +export class MockStarredEntitiesApi implements StarredEntitiesApi { + private readonly starredEntities = new Set(); + private readonly subscribers = new Set< + ZenObservable.SubscriptionObserver> + >(); + + private readonly observable = new ObservableImpl>(subscriber => { + subscriber.next(new Set(this.starredEntities)); + + this.subscribers.add(subscriber); + return () => { + this.subscribers.delete(subscriber); + }; + }); + + async toggleStarred(entityRef: string): Promise { + if (!this.starredEntities.delete(entityRef)) { + this.starredEntities.add(entityRef); + } + + for (const subscription of this.subscribers) { + subscription.next(new Set(this.starredEntities)); + } + } + + starredEntitie$(): Observable> { + return this.observable; + } +} diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts index e9f9c8923a..ba44c38e1d 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi'; export { starredEntitiesApiRef } from './StarredEntitiesApi'; export type { StarredEntitiesApi } from './StarredEntitiesApi'; +export { MockStarredEntitiesApi } from './MockStarredEntitiesApi'; diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index d025d08012..ec2c8126e5 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -31,7 +31,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useEntityList } from '../../hooks/useEntityListProvider'; import { EntityOwnerFilter } from '../../filters'; import { getEntityRelations } from '../../utils'; -import { formatEntityRefTitle } from '../EntityRefLink'; +import { humanizeEntityRef } from '../EntityRefLink'; /** @public */ export type CatalogReactEntityOwnerPickerClassKey = 'input'; @@ -86,7 +86,7 @@ export const EntityOwnerPicker = () => { backendEntities .flatMap((e: Entity) => getEntityRelations(e, RELATION_OWNED_BY).map(o => - formatEntityRefTitle(o, { defaultKind: 'group' }), + humanizeEntityRef(o, { defaultKind: 'group' }), ), ) .filter(Boolean) as string[], diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 1e01402ddf..06a632232e 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -16,13 +16,13 @@ import { Entity, - EntityName, + CompoundEntityRef, DEFAULT_NAMESPACE, parseEntityRef, } from '@backstage/catalog-model'; import React, { forwardRef } from 'react'; import { entityRouteRef } from '../../routes'; -import { formatEntityRefTitle } from './format'; +import { humanizeEntityRef } from './humanize'; import { Link, LinkProps } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; import { Tooltip } from '@material-ui/core'; @@ -33,7 +33,7 @@ import { Tooltip } from '@material-ui/core'; * @public */ export type EntityRefLinkProps = { - entityRef: Entity | EntityName | string; + entityRef: Entity | CompoundEntityRef | string; defaultKind?: string; title?: string; children?: React.ReactNode; @@ -72,7 +72,7 @@ export const EntityRefLink = forwardRef( namespace = namespace?.toLocaleLowerCase('en-US') ?? DEFAULT_NAMESPACE; const routeParams = { kind, namespace, name }; - const formattedEntityRefTitle = formatEntityRefTitle( + const formattedEntityRefTitle = humanizeEntityRef( { kind, namespace, name }, { defaultKind }, ); diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx index 9be970aa56..d92cd2ec87 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import React from 'react'; import { EntityRefLink } from './EntityRefLink'; import { LinkProps } from '@backstage/core-components'; @@ -25,7 +25,7 @@ import { LinkProps } from '@backstage/core-components'; * @public */ export type EntityRefLinksProps = { - entityRefs: (Entity | EntityName)[]; + entityRefs: (Entity | CompoundEntityRef)[]; defaultKind?: string; } & Omit; diff --git a/plugins/catalog-react/src/components/EntityRefLink/format.test.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts similarity index 86% rename from plugins/catalog-react/src/components/EntityRefLink/format.test.ts rename to plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts index b489f088fd..b541946ece 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/format.test.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { formatEntityRefTitle } from './format'; +import { humanizeEntityRef } from './humanize'; describe('formatEntityRefTitle', () => { it('formats entity in default namespace', () => { @@ -30,7 +30,7 @@ describe('formatEntityRefTitle', () => { lifecycle: 'production', }, }; - const title = formatEntityRefTitle(entity); + const title = humanizeEntityRef(entity); expect(title).toEqual('component:software'); }); @@ -48,7 +48,7 @@ describe('formatEntityRefTitle', () => { lifecycle: 'production', }, }; - const title = formatEntityRefTitle(entity); + const title = humanizeEntityRef(entity); expect(title).toEqual('component:test/software'); }); @@ -66,7 +66,7 @@ describe('formatEntityRefTitle', () => { lifecycle: 'production', }, }; - const title = formatEntityRefTitle(entity, { defaultKind: 'Component' }); + const title = humanizeEntityRef(entity, { defaultKind: 'Component' }); expect(title).toEqual('test/software'); }); @@ -76,7 +76,7 @@ describe('formatEntityRefTitle', () => { namespace: 'default', name: 'software', }; - const title = formatEntityRefTitle(entityName); + const title = humanizeEntityRef(entityName); expect(title).toEqual('component:software'); }); @@ -87,7 +87,7 @@ describe('formatEntityRefTitle', () => { name: 'software', }; - const title = formatEntityRefTitle(entityName); + const title = humanizeEntityRef(entityName); expect(title).toEqual('component:test/software'); }); @@ -98,7 +98,7 @@ describe('formatEntityRefTitle', () => { name: 'software', }; - const title = formatEntityRefTitle(entityName, { + const title = humanizeEntityRef(entityName, { defaultKind: 'component', }); expect(title).toEqual('test/software'); diff --git a/plugins/catalog-react/src/components/EntityRefLink/format.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts similarity index 85% rename from plugins/catalog-react/src/components/EntityRefLink/format.ts rename to plugins/catalog-react/src/components/EntityRefLink/humanize.ts index 90100245a1..0ade1da0b9 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/format.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -16,13 +16,16 @@ import { Entity, - EntityName, + CompoundEntityRef, DEFAULT_NAMESPACE, } from '@backstage/catalog-model'; +/** @public @deprecated please use {@link humanizeEntityRef} instead */ +export const formatEntityRefTitle = humanizeEntityRef; + /** @public */ -export function formatEntityRefTitle( - entityRef: Entity | EntityName, +export function humanizeEntityRef( + entityRef: Entity | CompoundEntityRef, opts?: { defaultKind?: string }, ) { const defaultKind = opts?.defaultKind; diff --git a/plugins/catalog-react/src/components/EntityRefLink/index.ts b/plugins/catalog-react/src/components/EntityRefLink/index.ts index d49993feb6..50394547a0 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/index.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/index.ts @@ -18,4 +18,4 @@ export { EntityRefLink } from './EntityRefLink'; export type { EntityRefLinkProps } from './EntityRefLink'; export { EntityRefLinks } from './EntityRefLinks'; export type { EntityRefLinksProps } from './EntityRefLinks'; -export { formatEntityRefTitle } from './format'; +export { humanizeEntityRef, formatEntityRefTitle } from './humanize'; diff --git a/plugins/catalog-react/src/components/EntityTable/columns.tsx b/plugins/catalog-react/src/components/EntityTable/columns.tsx index d6c3582af6..08eb31bea3 100644 --- a/plugins/catalog-react/src/components/EntityTable/columns.tsx +++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx @@ -16,7 +16,7 @@ import { Entity, - EntityName, + CompoundEntityRef, RELATION_OWNED_BY, RELATION_PART_OF, } from '@backstage/catalog-model'; @@ -26,7 +26,7 @@ import { getEntityRelations } from '../../utils'; import { EntityRefLink, EntityRefLinks, - formatEntityRefTitle, + humanizeEntityRef, } from '../EntityRefLink'; /** @public */ @@ -38,7 +38,7 @@ export const columnFactories = Object.freeze({ function formatContent(entity: T): string { return ( entity.metadata?.title || - formatEntityRefTitle(entity, { + humanizeEntityRef(entity, { defaultKind, }) ); @@ -81,13 +81,13 @@ export const columnFactories = Object.freeze({ defaultKind?: string; filter?: { kind: string }; }): TableColumn { - function getRelations(entity: T): EntityName[] { + function getRelations(entity: T): CompoundEntityRef[] { return getEntityRelations(entity, relation, entityFilter); } function formatContent(entity: T): string { return getRelations(entity) - .map(r => formatEntityRefTitle(r, { defaultKind })) + .map(r => humanizeEntityRef(r, { defaultKind })) .join(', '); } diff --git a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx index 3832a82877..eaa2821448 100644 --- a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx +++ b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx @@ -32,11 +32,17 @@ const YellowStar = withStyles({ }, })(Star); -/** @public */ +/** + * @public + * @deprecated due to low utility value. + */ export const favoriteEntityTooltip = (isStarred: boolean) => isStarred ? 'Remove from favorites' : 'Add to favorites'; -/** @public */ +/** + * @public + * @deprecated due to low utility value. + */ export const favoriteEntityIcon = (isStarred: boolean) => isStarred ? : ; @@ -55,8 +61,10 @@ export const FavoriteEntity = (props: FavoriteEntityProps) => { {...props} onClick={() => toggleStarredEntity()} > - - {favoriteEntityIcon(isStarredEntity)} + + {isStarredEntity ? : } ); diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx index bf6c8971b0..0423c0da44 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx @@ -33,7 +33,7 @@ import React, { useLayoutEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router'; import useAsync from 'react-use/lib/useAsync'; import { catalogApiRef } from '../../../api'; -import { formatEntityRefTitle } from '../../../components/EntityRefLink/format'; +import { humanizeEntityRef } from '../../EntityRefLink'; import { entityRouteRef } from '../../../routes'; import { EntityKindIcon } from './EntityKindIcon'; @@ -132,7 +132,7 @@ function CustomNode({ node }: DependencyGraphTypes.RenderNodeProps) { const displayTitle = node.metadata.title || (node.kind && node.metadata.name && node.metadata.namespace - ? formatEntityRefTitle({ + ? humanizeEntityRef({ kind: node.kind, name: node.metadata.name, namespace: node.metadata.namespace || '', diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts index 6420ebd5ae..c14b6523c0 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts @@ -16,8 +16,8 @@ import { Entity, - EntityName, - getEntityName, + CompoundEntityRef, + getCompoundEntityRef, ANNOTATION_ORIGIN_LOCATION, } from '@backstage/catalog-model'; import { catalogApiRef } from '../../api'; @@ -44,7 +44,7 @@ export type UseUnregisterEntityDialogState = | { type: 'unregister'; location: string; - colocatedEntities: EntityName[]; + colocatedEntities: CompoundEntityRef[]; unregisterLocation: () => Promise; deleteEntity: () => Promise; } @@ -141,7 +141,7 @@ export function useUnregisterEntityDialogState( return { type: 'unregister', location: locationRef!, - colocatedEntities: colocatedEntities.map(getEntityName), + colocatedEntities: colocatedEntities.map(getCompoundEntityRef), unregisterLocation, deleteEntity, }; diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 316a0f2c06..310deabbb7 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -55,7 +55,7 @@ const mockConfigApi = { } as Partial; const mockCatalogApi = { - getEntityByName: () => Promise.resolve(mockUser), + getEntityByRef: () => Promise.resolve(mockUser), } as Partial; const mockIdentityApi = { diff --git a/plugins/catalog-react/src/filters.test.ts b/plugins/catalog-react/src/filters.test.ts index cdc2ce2150..ba31e21122 100644 --- a/plugins/catalog-react/src/filters.test.ts +++ b/plugins/catalog-react/src/filters.test.ts @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { EntityTextFilter } from './filters'; const entities: Entity[] = [ @@ -37,9 +37,9 @@ const entities: Entity[] = [ }, ]; -const templates: TemplateEntityV1beta2[] = [ +const templates: TemplateEntityV1beta3[] = [ { - apiVersion: 'backstage.io/v1beta2', + apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', metadata: { name: 'react-app', @@ -52,7 +52,7 @@ const templates: TemplateEntityV1beta2[] = [ }, }, { - apiVersion: 'backstage.io/v1beta2', + apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', metadata: { name: 'gRPC service', diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 5d2c2e531b..9fb057a01c 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -15,7 +15,7 @@ */ import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { formatEntityRefTitle } from './components/EntityRefLink'; +import { humanizeEntityRef } from './components/EntityRefLink'; import { EntityFilter, UserListFilterKind } from './types'; import { getEntityRelations } from './utils'; @@ -107,7 +107,7 @@ export class EntityOwnerFilter implements EntityFilter { filterEntity(entity: Entity): boolean { return this.values.some(v => getEntityRelations(entity, RELATION_OWNED_BY).some( - o => formatEntityRefTitle(o, { defaultKind: 'group' }) === v, + o => humanizeEntityRef(o, { defaultKind: 'group' }) === v, ), ); } diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index 3e6ef7c945..3570ed897c 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -15,20 +15,18 @@ */ export { useEntity, - useEntityFromUrl, EntityProvider, AsyncEntityProvider, + useAsyncEntity, } from './useEntity'; export type { EntityLoadingStatus, EntityProviderProps, AsyncEntityProviderProps, } from './useEntity'; -export { useEntityCompoundName } from './useEntityCompoundName'; export { EntityListContext, EntityListProvider, - useEntityListProvider, useEntityList, } from './useEntityListProvider'; export type { @@ -36,12 +34,8 @@ export type { EntityListContextProps, } from './useEntityListProvider'; export { useEntityTypeFilter } from './useEntityTypeFilter'; -export type { EntityTypeReturn } from './useEntityTypeFilter'; -export { useEntityKinds } from './useEntityKinds'; -export { useOwnUser } from './useOwnUser'; export { useRelatedEntities } from './useRelatedEntities'; export { useStarredEntities } from './useStarredEntities'; export { useStarredEntity } from './useStarredEntity'; -export { loadCatalogOwnerRefs, useEntityOwnership } from './useEntityOwnership'; -export { useOwnedEntities } from './useOwnedEntities'; +export { useEntityOwnership } from './useEntityOwnership'; export { useEntityPermission } from './useEntityPermission'; diff --git a/plugins/catalog-react/src/hooks/useEntity.tsx b/plugins/catalog-react/src/hooks/useEntity.tsx index 5cb67cb79a..75a623aaed 100644 --- a/plugins/catalog-react/src/hooks/useEntity.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.tsx @@ -14,21 +14,16 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import { errorApiRef, useApi } from '@backstage/core-plugin-api'; import { createVersionedContext, createVersionedValueMap, useVersionedContext, } from '@backstage/version-bridge'; -import React, { ReactNode, useEffect } from 'react'; -import { useNavigate } from 'react-router'; -import useAsyncRetry from 'react-use/lib/useAsyncRetry'; -import { catalogApiRef } from '../api'; -import { useEntityCompoundName } from './useEntityCompoundName'; +import React, { ReactNode } from 'react'; /** @public */ -export type EntityLoadingStatus = { - entity?: Entity; +export type EntityLoadingStatus = { + entity?: TEntity; loading: boolean; error?: Error; refresh?: VoidFunction; @@ -100,54 +95,26 @@ export const EntityProvider = ({ entity, children }: EntityProviderProps) => ( /> ); -/** @public - * @deprecated will be deleted shortly due to low external usage, re-implement if needed. - */ -export const useEntityFromUrl = (): EntityLoadingStatus => { - const { kind, namespace, name } = useEntityCompoundName(); - const navigate = useNavigate(); - const errorApi = useApi(errorApiRef); - const catalogApi = useApi(catalogApiRef); - - const { - value: entity, - error, - loading, - retry: refresh, - } = useAsyncRetry( - () => catalogApi.getEntityByName({ kind, namespace, name }), - [catalogApi, kind, namespace, name], - ); - - useEffect(() => { - if (!name) { - errorApi.post(new Error('No name provided!')); - navigate('/'); - } - }, [errorApi, navigate, error, loading, entity, name]); - - return { entity, loading, error, refresh }; -}; - /** - * Grab the current entity from the context and its current loading state. + * Grab the current entity from the context, throws if the entity has not yet been loaded + * or is not available. * * @public */ -export function useEntity() { +export function useEntity(): { + entity: TEntity; + /** @deprecated use {@link useAsyncEntity} instead */ + loading: boolean; + /** @deprecated use {@link useAsyncEntity} instead */ + error?: Error; + /** @deprecated use {@link useAsyncEntity} instead */ + refresh?: VoidFunction; +} { const versionedHolder = useVersionedContext<{ 1: EntityLoadingStatus }>('entity-context'); if (!versionedHolder) { - // TODO(Rugvip): Throw this once we fully migrate to the new context - // throw new Error('Entity context is not available'); - - return { - entity: undefined as unknown as T, - loading: true, - error: undefined, - refresh: () => {}, - }; + throw new Error('Entity context is not available'); } const value = versionedHolder.atVersion(1); @@ -155,6 +122,40 @@ export function useEntity() { throw new Error('EntityContext v1 not available'); } + if (!value.entity) { + // Once we have removed the additional fields from being returned we can drop this deprecation + // and move to the error instead. + // throw new Error('useEntity hook is being called outside of an EntityLayout where the entity has not been loaded. If this is intentional, please use useAsyncEntity instead.'); + + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION: useEntity hook is being called outside of an EntityLayout where the entity has not been loaded. If this is intentional, please use useAsyncEntity instead. This warning will be replaced with an error in future releases.', + ); + } + const { entity, loading, error, refresh } = value; - return { entity: entity as T, loading, error, refresh }; + return { entity: entity as TEntity, loading, error, refresh }; +} + +/** + * Grab the current entity from the context, provides loading state and errors, and the ability to refresh. + * + * @public + */ +export function useAsyncEntity< + TEntity extends Entity = Entity, +>(): EntityLoadingStatus { + const versionedHolder = + useVersionedContext<{ 1: EntityLoadingStatus }>('entity-context'); + + if (!versionedHolder) { + throw new Error('Entity context is not available'); + } + const value = versionedHolder.atVersion(1); + if (!value) { + throw new Error('EntityContext v1 not available'); + } + + const { entity, loading, error, refresh } = value; + return { entity: entity as TEntity, loading, error, refresh }; } diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx deleted file mode 100644 index c96301b3c4..0000000000 --- a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { PropsWithChildren } from 'react'; -import { CatalogApi } from '@backstage/catalog-client'; -import { catalogApiRef } from '../api'; -import { renderHook } from '@testing-library/react-hooks'; -import { useEntityKinds } from './useEntityKinds'; -import { TestApiProvider } from '@backstage/test-utils'; - -const mockCatalogApi: Partial = { - getEntityFacets: jest.fn().mockResolvedValue({ - facets: { - kind: [ - { value: 'Template', count: 2 }, - { value: 'System', count: 1 }, - { value: 'Component', count: 3 }, - ], - }, - }), -}; - -const wrapper = ({ children }: PropsWithChildren<{}>) => { - return ( - - {children} - - ); -}; - -describe('useEntityKinds', () => { - it('gets entity kinds', async () => { - const { result, waitForValueToChange } = renderHook( - () => useEntityKinds(), - { wrapper }, - ); - await waitForValueToChange(() => result.current); - expect(result.current.kinds).toEqual(['Component', 'System', 'Template']); - }); -}); diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.ts b/plugins/catalog-react/src/hooks/useEntityKinds.ts deleted file mode 100644 index 3dfc31eb88..0000000000 --- a/plugins/catalog-react/src/hooks/useEntityKinds.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import useAsync from 'react-use/lib/useAsync'; -import { useApi } from '@backstage/core-plugin-api'; -import { catalogApiRef } from '../api'; - -/** - * Retrieve a list of unique entity kinds present in the catalog - * @public - * @deprecated and will be removed due to low utility value. - */ -export function useEntityKinds() { - const catalogApi = useApi(catalogApiRef); - - const { - error, - loading, - value: kinds, - } = useAsync(async () => { - return await catalogApi - .getEntityFacets({ facets: ['kind'] }) - .then(response => response.facets.kind?.map(f => f.value).sort() || []); - }); - return { error, loading, kinds }; -} diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index c8ca29b07f..e3ab09f081 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -29,7 +29,7 @@ import qs from 'qs'; import React, { PropsWithChildren } from 'react'; import { MemoryRouter } from 'react-router'; import { catalogApiRef } from '../api'; -import { DefaultStarredEntitiesApi, starredEntitiesApiRef } from '../apis'; +import { starredEntitiesApiRef, MockStarredEntitiesApi } from '../apis'; import { EntityKindPicker, UserListPicker } from '../components'; import { EntityKindFilter, EntityTypeFilter, UserListFilter } from '../filters'; import { UserListFilterKind } from '../types'; @@ -76,7 +76,7 @@ const mockIdentityApi: Partial = { }; const mockCatalogApi: Partial = { getEntities: jest.fn().mockImplementation(async () => ({ items: entities })), - getEntityByName: async () => undefined, + getEntityByRef: async () => undefined, }; const wrapper = ({ @@ -95,12 +95,7 @@ const wrapper = ({ [catalogApiRef, mockCatalogApi], [identityApiRef, mockIdentityApi], [storageApiRef, MockStorageApi.create()], - [ - starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), - }), - ], + [starredEntitiesApiRef, new MockStarredEntitiesApi()], ]} > diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index f448e41b82..ccac475ae8 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -195,7 +195,7 @@ export const EntityListProvider = ({ }); const newParams = qs.stringify( { ...oldParams, filters: queryParams }, - { addQueryPrefix: true }, + { addQueryPrefix: true, arrayFormat: 'repeat' }, ); const newUrl = `${window.location.pathname}${newParams}`; // We use direct history manipulation since useSearchParams and @@ -249,20 +249,6 @@ export const EntityListProvider = ({ ); }; -/** - * Hook for interacting with the entity list context provided by the {@link EntityListProvider}. - * @public - * @deprecated use {@link useEntityList} instead. - */ -export function useEntityListProvider< - EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, ->(): EntityListContextProps { - const context = useContext(EntityListContext); - if (!context) - throw new Error('useEntityList must be used within EntityListProvider'); - return context; -} - /** * Hook for interacting with the entity list context provided by the {@link EntityListProvider}. * @public diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx index 1bb9a649ad..d4d80203d3 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx @@ -15,28 +15,23 @@ */ import { CatalogApi } from '@backstage/catalog-client'; -import { - ComponentEntity, - RELATION_MEMBER_OF, - RELATION_OWNED_BY, - UserEntity, -} from '@backstage/catalog-model'; +import { ComponentEntity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; import { TestApiProvider } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { catalogApiRef } from '../api'; -import { loadCatalogOwnerRefs, useEntityOwnership } from './useEntityOwnership'; +import { useEntityOwnership } from './useEntityOwnership'; describe('useEntityOwnership', () => { type MockIdentityApi = jest.Mocked>; - type MockCatalogApi = jest.Mocked>; + type MockCatalogApi = jest.Mocked>; const mockIdentityApi: MockIdentityApi = { getBackstageIdentity: jest.fn(), }; const mockCatalogApi: MockCatalogApi = { - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), }; const identityApi = mockIdentityApi as unknown as IdentityApi; @@ -77,55 +72,10 @@ describe('useEntityOwnership', () => { ], }; - const user2Entity: UserEntity = { - apiVersion: 'backstage.io/v1beta1', - kind: 'User', - metadata: { - name: 'user2', - namespace: 'default', - }, - spec: { - /* should not be accessed */ - } as any, - relations: [ - { - type: RELATION_MEMBER_OF, - targetRef: 'group:default/group1', - target: { kind: 'Group', namespace: 'default', name: 'group1' }, - }, - ], - }; - afterEach(() => { jest.resetAllMocks(); }); - describe('loadCatalogOwnerRefs', () => { - it('loads the first user from the catalog', async () => { - mockCatalogApi.getEntityByName.mockResolvedValueOnce(user2Entity); - await expect( - loadCatalogOwnerRefs(catalogApi, ['user:default/user2']), - ).resolves.toEqual(['group:default/group1']); - expect(mockCatalogApi.getEntityByName).toBeCalledWith({ - kind: 'user', - namespace: 'default', - name: 'user2', - }); - }); - - it('gracefully handles missing user', async () => { - mockCatalogApi.getEntityByName.mockResolvedValueOnce(undefined); - await expect( - loadCatalogOwnerRefs(catalogApi, ['user:default/user2']), - ).resolves.toEqual([]); - expect(mockCatalogApi.getEntityByName).toBeCalledWith({ - kind: 'user', - namespace: 'default', - name: 'user2', - }); - }); - }); - describe('useEntityOwnership', () => { it('matches ownership via ownership entity refs', async () => { mockIdentityApi.getBackstageIdentity.mockResolvedValue({ @@ -133,7 +83,7 @@ describe('useEntityOwnership', () => { userEntityRef: 'user:default/user1', ownershipEntityRefs: ['user:default/user1', 'group:default/group1'], }); - mockCatalogApi.getEntityByName.mockResolvedValue(undefined); + mockCatalogApi.getEntityByRef.mockResolvedValue(undefined); const { result, waitForValueToChange } = renderHook( () => useEntityOwnership(), diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.ts b/plugins/catalog-react/src/hooks/useEntityOwnership.ts index 77f6c4bf24..f8a37a2c83 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.ts +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.ts @@ -14,57 +14,16 @@ * limitations under the License. */ -import { CatalogApi } from '@backstage/catalog-client'; import { Entity, - EntityName, - parseEntityRef, - RELATION_MEMBER_OF, RELATION_OWNED_BY, stringifyEntityRef, } from '@backstage/catalog-model'; import { identityApiRef, useApi } from '@backstage/core-plugin-api'; import { useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; -import { catalogApiRef } from '../api'; import { getEntityRelations } from '../utils/getEntityRelations'; -/** - * Takes the relevant parts of the User entity corresponding to the Backstage - * identity, and translates them into a list of entity refs on string form that - * represent the user's ownership connections. - * - * @public - * - * @param catalogApi - The Catalog API implementation - * @param identityOwnerRefs - List of identity owner refs as strings - * @returns OwnerRefs as a string array - * @deprecated Use `ownershipEntityRefs` from `identityApi.getBackstageIdentity()` instead. - */ -export async function loadCatalogOwnerRefs( - catalogApi: CatalogApi, - identityOwnerRefs: string[], -): Promise { - const result = new Array(); - - const primaryUserRef = identityOwnerRefs.find(ref => ref.startsWith('user:')); - if (primaryUserRef) { - const entity = await catalogApi.getEntityByName( - parseEntityRef(primaryUserRef), - ); - if (entity) { - const memberOf = getEntityRelations(entity, RELATION_MEMBER_OF, { - kind: 'Group', - }); - for (const group of memberOf) { - result.push(stringifyEntityRef(group)); - } - } - } - - return result; -} - /** * Returns a function that checks whether the currently signed-in user is an * owner of a given entity. When the hook is initially mounted, the loading @@ -77,29 +36,22 @@ export async function loadCatalogOwnerRefs( */ export function useEntityOwnership(): { loading: boolean; - isOwnedEntity: (entity: Entity | EntityName) => boolean; + isOwnedEntity: (entity: Entity) => boolean; } { const identityApi = useApi(identityApiRef); - const catalogApi = useApi(catalogApiRef); // Trigger load only on mount const { loading, value: refs } = useAsync(async () => { const { ownershipEntityRefs } = await identityApi.getBackstageIdentity(); - const catalogRefs = await loadCatalogOwnerRefs( - catalogApi, - ownershipEntityRefs, - ); - return new Set([...ownershipEntityRefs, ...catalogRefs]); + return ownershipEntityRefs; }, []); const isOwnedEntity = useMemo(() => { const myOwnerRefs = new Set(refs ?? []); - return (entity: Entity | EntityName) => { - const entityOwnerRefs = ( - 'metadata' in entity - ? getEntityRelations(entity, RELATION_OWNED_BY) - : [entity] - ).map(stringifyEntityRef); + return (entity: Entity) => { + const entityOwnerRefs = getEntityRelations(entity, RELATION_OWNED_BY).map( + stringifyEntityRef, + ); for (const ref of entityOwnerRefs) { if (myOwnerRefs.has(ref)) { return true; diff --git a/plugins/catalog-react/src/hooks/useEntityPermission.ts b/plugins/catalog-react/src/hooks/useEntityPermission.ts index c1f62cfc39..092114936a 100644 --- a/plugins/catalog-react/src/hooks/useEntityPermission.ts +++ b/plugins/catalog-react/src/hooks/useEntityPermission.ts @@ -28,7 +28,7 @@ import { useEntity } from './useEntity'; * Note: this hook blocks the permission request until the entity has loaded in * context. If you have the entityRef and need concurrent requests, use the * `usePermission` hook directly. - * @public + * @alpha */ export function useEntityPermission(permission: Permission): { loading: boolean; diff --git a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx index 2159c30edc..9971536138 100644 --- a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx +++ b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx @@ -23,17 +23,6 @@ import { catalogApiRef } from '../api'; import { useEntityList } from './useEntityListProvider'; import { EntityTypeFilter } from '../filters'; -/** @public - * @deprecated type inlined with {@link useEntityTypeFilter}. - */ -export type EntityTypeReturn = { - loading: boolean; - error?: Error; - availableTypes: string[]; - selectedTypes: string[]; - setSelectedTypes: (types: string[]) => void; -}; - /** * A hook built on top of `useEntityList` for enabling selection of valid `spec.type` values * based on the selected EntityKindFilter. diff --git a/plugins/catalog-react/src/hooks/useOwnUser.ts b/plugins/catalog-react/src/hooks/useOwnUser.ts deleted file mode 100644 index 74c44bc638..0000000000 --- a/plugins/catalog-react/src/hooks/useOwnUser.ts +++ /dev/null @@ -1,44 +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 { - DEFAULT_NAMESPACE, - parseEntityRef, - UserEntity, -} from '@backstage/catalog-model'; -import useAsync, { AsyncState } from 'react-use/lib/useAsync'; -import { catalogApiRef } from '../api'; -import { identityApiRef, useApi } from '@backstage/core-plugin-api'; - -/** - * Get the catalog User entity (if any) that matches the logged-in user. - * @public - * @deprecated due to low external usage. - */ -export function useOwnUser(): AsyncState { - const catalogApi = useApi(catalogApiRef); - const identityApi = useApi(identityApiRef); - - return useAsync(async () => { - const identity = await identityApi.getBackstageIdentity(); - return catalogApi.getEntityByName( - parseEntityRef(identity.userEntityRef, { - defaultKind: 'User', - defaultNamespace: DEFAULT_NAMESPACE, - }), - ) as Promise; - }, [catalogApi, identityApi]); -} diff --git a/plugins/catalog-react/src/hooks/useOwnedEntities.ts b/plugins/catalog-react/src/hooks/useOwnedEntities.ts deleted file mode 100644 index a2258cf138..0000000000 --- a/plugins/catalog-react/src/hooks/useOwnedEntities.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { catalogApiRef } from './../api'; -import { loadCatalogOwnerRefs } from './useEntityOwnership'; -import { identityApiRef, useApi } from '@backstage/core-plugin-api'; -import { RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { GetEntitiesResponse } from '@backstage/catalog-client'; -import useAsync from 'react-use/lib/useAsync'; -import { useMemo } from 'react'; - -/** - * Takes the relevant parts of the Backstage identity, and translates them into - * a list of entities which are owned by the user. Takes an optional parameter - * to filter the entities based on allowedKinds - * - * @public - * - * @param allowedKinds - Array of allowed kinds to filter the entities - * @deprecated Use `ownershipEntityRefs` from `identityApi.getBackstageIdentity()` instead. - */ -export function useOwnedEntities(allowedKinds?: string[]): { - loading: boolean; - ownedEntities: GetEntitiesResponse | undefined; -} { - const identityApi = useApi(identityApiRef); - const catalogApi = useApi(catalogApiRef); - - const { loading, value: refs } = useAsync(async () => { - const identity = await identityApi.getBackstageIdentity(); - const identityRefs = identity.ownershipEntityRefs; - const catalogRefs = await loadCatalogOwnerRefs(catalogApi, identityRefs); - const catalogs = await catalogApi.getEntities( - allowedKinds - ? { - filter: { - kind: allowedKinds, - [`relations.${RELATION_OWNED_BY}`]: - [...identityRefs, ...catalogRefs] || [], - }, - } - : { - filter: { - [`relations.${RELATION_OWNED_BY}`]: - [...identityRefs, ...catalogRefs] || [], - }, - }, - ); - return catalogs; - }, []); - - const ownedEntities = useMemo(() => { - return refs; - }, [refs]); - - return useMemo(() => ({ loading, ownedEntities }), [loading, ownedEntities]); -} diff --git a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx index aef876a1b2..16fa19a1cd 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx @@ -15,15 +15,18 @@ */ import { Entity } from '@backstage/catalog-model'; -import { StorageApi } from '@backstage/core-plugin-api'; -import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; +import { TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; -import { DefaultStarredEntitiesApi, starredEntitiesApiRef } from '../apis'; +import { + starredEntitiesApiRef, + StarredEntitiesApi, + MockStarredEntitiesApi, +} from '../apis'; import { useStarredEntities } from './useStarredEntities'; describe('useStarredEntities', () => { - let mockStorage: StorageApi; + let mockApi: StarredEntitiesApi; let wrapper: React.ComponentType; const mockEntity: Entity = { @@ -44,22 +47,15 @@ describe('useStarredEntities', () => { }; beforeEach(() => { - mockStorage = MockStorageApi.create(); + mockApi = new MockStarredEntitiesApi(); wrapper = ({ children }: PropsWithChildren<{}>) => ( - + {children} ); }); - it('should return an empty set for when there is no items in storage', async () => { + it('should return an empty set', async () => { const { result, waitForNextUpdate } = renderHook( () => useStarredEntities(), { wrapper }, @@ -70,10 +66,11 @@ describe('useStarredEntities', () => { expect(result.current.starredEntities.size).toBe(0); }); - it('should return a set with the current items when there are items in storage', async () => { + it('should return a set with the current items', async () => { const expectedIds = ['i', 'am', 'some', 'test', 'ids']; - const store = mockStorage?.forBucket('starredEntities'); - await store?.set('entityRefs', expectedIds); + for (const id of expectedIds) { + mockApi.toggleStarred(id); + } const { result, waitForNextUpdate } = renderHook( () => useStarredEntities(), diff --git a/plugins/catalog-react/src/hooks/useStarredEntities.ts b/plugins/catalog-react/src/hooks/useStarredEntities.ts index 5ddabfdd2e..328a26e0f8 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntities.ts +++ b/plugins/catalog-react/src/hooks/useStarredEntities.ts @@ -16,7 +16,7 @@ import { Entity, - EntityName, + CompoundEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; @@ -24,7 +24,9 @@ import { useCallback } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { starredEntitiesApiRef } from '../apis'; -function getEntityRef(entityOrRef: Entity | EntityName | string): string { +function getEntityRef( + entityOrRef: Entity | CompoundEntityRef | string, +): string { return typeof entityOrRef === 'string' ? entityOrRef : stringifyEntityRef(entityOrRef); @@ -33,8 +35,12 @@ function getEntityRef(entityOrRef: Entity | EntityName | string): string { /** @public */ export function useStarredEntities(): { starredEntities: Set; - toggleStarredEntity: (entityOrRef: Entity | EntityName | string) => void; - isStarredEntity: (entityOrRef: Entity | EntityName | string) => boolean; + toggleStarredEntity: ( + entityOrRef: Entity | CompoundEntityRef | string, + ) => void; + isStarredEntity: ( + entityOrRef: Entity | CompoundEntityRef | string, + ) => boolean; } { const starredEntitiesApi = useApi(starredEntitiesApiRef); @@ -44,13 +50,13 @@ export function useStarredEntities(): { ); const isStarredEntity = useCallback( - (entityOrRef: Entity | EntityName | string) => + (entityOrRef: Entity | CompoundEntityRef | string) => starredEntities.has(getEntityRef(entityOrRef)), [starredEntities], ); const toggleStarredEntity = useCallback( - (entityOrRef: Entity | EntityName | string) => + (entityOrRef: Entity | CompoundEntityRef | string) => starredEntitiesApi.toggleStarred(getEntityRef(entityOrRef)).then(), [starredEntitiesApi], ); diff --git a/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx index 8ffc891092..317a0e391c 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { TestApiProvider } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; @@ -44,7 +44,7 @@ describe('useStarredEntity', () => { describe.each` title | entityOrRef ${'entity reference'} | ${'component:default/mock'} - ${'entity name'} | ${{ kind: 'component', namespace: 'default', name: 'mock' } as EntityName} + ${'entity name'} | ${{ kind: 'component', namespace: 'default', name: 'mock' } as CompoundEntityRef} ${'entity'} | ${{ apiVersion: '1', kind: 'Component', metadata: { name: 'mock' } } as Entity} `('with $title', ({ entityOrRef }) => { describe('toggleStarredEntity', () => { diff --git a/plugins/catalog-react/src/hooks/useStarredEntity.ts b/plugins/catalog-react/src/hooks/useStarredEntity.ts index 2c647163aa..23d5677512 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntity.ts +++ b/plugins/catalog-react/src/hooks/useStarredEntity.ts @@ -16,21 +16,25 @@ import { Entity, - EntityName, + CompoundEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { useCallback, useEffect, useState } from 'react'; import { starredEntitiesApiRef } from '../apis'; -function getEntityRef(entityOrRef: Entity | EntityName | string): string { +function getEntityRef( + entityOrRef: Entity | CompoundEntityRef | string, +): string { return typeof entityOrRef === 'string' ? entityOrRef : stringifyEntityRef(entityOrRef); } /** @public */ -export function useStarredEntity(entityOrRef: Entity | EntityName | string): { +export function useStarredEntity( + entityOrRef: Entity | CompoundEntityRef | string, +): { toggleStarredEntity: () => void; isStarredEntity: boolean; } { diff --git a/plugins/catalog-react/src/index.ts b/plugins/catalog-react/src/index.ts index 7bf1bd1221..6d5a5ad204 100644 --- a/plugins/catalog-react/src/index.ts +++ b/plugins/catalog-react/src/index.ts @@ -27,14 +27,15 @@ export * from './apis'; export * from './components'; export * from './hooks'; export * from './filters'; -export { - catalogRouteRef, - entityRoute, - entityRouteParams, - entityRouteRef, - rootRoute, -} from './routes'; +export { entityRouteParams, entityRouteRef } from './routes'; export * from './testUtils'; export * from './types'; -export * from './utils'; export * from './overridableComponents'; +export { + getEntityMetadataEditUrl, + getEntityMetadataViewUrl, + getEntityRelations, + getEntitySourceLocation, + isOwnerOf, +} from './utils'; +export type { EntitySourceLocation } from './utils'; diff --git a/plugins/catalog-react/src/routes.ts b/plugins/catalog-react/src/routes.ts index b9aa0de531..013bf7b5be 100644 --- a/plugins/catalog-react/src/routes.ts +++ b/plugins/catalog-react/src/routes.ts @@ -18,21 +18,6 @@ import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { createRouteRef } from '@backstage/core-plugin-api'; import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; -// TODO(Rugvip): Move these route refs back to the catalog plugin once we're all ported to using external routes -/** - * @deprecated Use an `ExternalRouteRef` instead, which can point to `catalogPlugin.routes.catalogIndex`. - * @public - */ -export const rootRoute = createRouteRef({ - id: 'catalog', -}); - -/** - * @deprecated Use an `ExternalRouteRef` instead, which can point to `catalogPlugin.routes.catalogIndex`. - * @public - */ -export const catalogRouteRef = rootRoute; - /** * A stable route ref that points to the catalog page for an individual entity. * @@ -52,12 +37,6 @@ export const entityRouteRef = getOrCreateGlobalSingleton( }), ); -/** - * @deprecated use `entityRouteRef` instead. - * @public - */ -export const entityRoute = entityRouteRef; - /** * Utility function to get suitable route params for entityRoute, given an * @public diff --git a/plugins/catalog-react/src/utils/filters.ts b/plugins/catalog-react/src/utils/filters.ts index 68cd94fd6e..109e864c52 100644 --- a/plugins/catalog-react/src/utils/filters.ts +++ b/plugins/catalog-react/src/utils/filters.ts @@ -17,10 +17,6 @@ import { Entity } from '@backstage/catalog-model'; import { EntityFilter } from '../types'; -/** - * @public - * @deprecated will be made private. - */ export function reduceCatalogFilters( filters: EntityFilter[], ): Record { @@ -32,10 +28,6 @@ export function reduceCatalogFilters( }, {} as Record); } -/** - * @public - * @deprecated will be made private. - */ export function reduceEntityFilters( filters: EntityFilter[], ): (entity: Entity) => boolean { diff --git a/plugins/catalog-react/src/utils/getEntityRelations.ts b/plugins/catalog-react/src/utils/getEntityRelations.ts index 45cbc6967f..13086db4ea 100644 --- a/plugins/catalog-react/src/utils/getEntityRelations.ts +++ b/plugins/catalog-react/src/utils/getEntityRelations.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { Entity, EntityName, parseEntityRef } from '@backstage/catalog-model'; +import { + Entity, + CompoundEntityRef, + parseEntityRef, +} from '@backstage/catalog-model'; // TODO(freben): This should be returning entity refs instead /** @@ -26,7 +30,7 @@ export function getEntityRelations( entity: Entity | undefined, relationType: string, filter?: { kind: string }, -): EntityName[] { +): CompoundEntityRef[] { let entityNames = entity?.relations ?.filter(r => r.type === relationType) diff --git a/plugins/catalog-react/src/utils/isOwnerOf.ts b/plugins/catalog-react/src/utils/isOwnerOf.ts index 3ef6b4b2a0..2b38ace247 100644 --- a/plugins/catalog-react/src/utils/isOwnerOf.ts +++ b/plugins/catalog-react/src/utils/isOwnerOf.ts @@ -16,7 +16,7 @@ import { Entity, - getEntityName, + getCompoundEntityRef, RELATION_MEMBER_OF, RELATION_OWNED_BY, stringifyEntityRef, @@ -24,18 +24,23 @@ import { import { getEntityRelations } from './getEntityRelations'; /** - * Get the related entity references. - * @public + * Returns true if the `owner` argument is a direct owner on the `entity` argument. + * + * @alpha + * @remarks + * + * Note that this ownership is not the same as using the claims in the auth-resolver, it only will take into account ownership as expressed by direct entity relations. + * It doesn't know anything about the additional groups that a user might belong to which the claims contain. */ -export function isOwnerOf(owner: Entity, owned: Entity) { +export function isOwnerOf(owner: Entity, entity: Entity) { const possibleOwners = new Set( [ ...getEntityRelations(owner, RELATION_MEMBER_OF, { kind: 'group' }), - ...(owner ? [getEntityName(owner)] : []), + ...(owner ? [getCompoundEntityRef(owner)] : []), ].map(stringifyEntityRef), ); - const owners = getEntityRelations(owned, RELATION_OWNED_BY).map( + const owners = getEntityRelations(entity, RELATION_OWNED_BY).map( stringifyEntityRef, ); diff --git a/plugins/catalog/.eslintrc.js b/plugins/catalog/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/catalog/.eslintrc.js +++ b/plugins/catalog/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 3c70959b78..ed10e6723b 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-catalog +## 0.10.0-next.0 + +### Minor Changes + +- 51856359bf: **BREAKING**: Removed the `AboutCard` component which has been replaced by `EntityAboutCard`. +- 5ea9509e6a: **BREAKING**: Removed `CatalogResultListItemProps` and `CatalogResultListItem`, replaced by `CatalogSearchResultListItemProps` and `CatalogSearchResultListItem`. + +### Patch Changes + +- 251688a75e: Updated `CatalogKindHeader` to respond to external changes to query parameters in the URL, such as two sidebar links that apply different catalog filters. +- 9844d4d2bd: Removed usage of removed hook. +- 3e54f6c436: Use `@backstage/plugin-search-common` package instead of `@backstage/search-common`. +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/plugin-catalog-common@0.2.2-next.0 + - @backstage/plugin-search-common@0.3.1-next.0 + - @backstage/catalog-client@0.9.0-next.0 + - @backstage/integration-react@0.1.25-next.0 + +## 0.9.1 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- f590d1681b: Removed usage of deprecated favorite utility methods. +- 44403296e7: Added the following deprecations to the `catalog-react` package: + + - **DEPRECATION**: `useEntity` will now warn if the entity has not yet been loaded, and will soon throw errors instead. If you're using the default implementation of `EntityLayout` and `EntitySwitch` then these components will ensure that there is an entity loaded before rendering children. If you're implementing your own `EntityLayout` or `EntitySwitch` or something that operates outside or adjacent to them, then use `useAsyncEntity`. + + - **DEPRECATION**: the `loading`, `error` and `refresh` properties that are returned from `useEntity` have been deprecated, and are available on `useAsyncEntity` instead. + +- da79aac2a6: - Replaced usage of the deprecated and now removed `rootRoute` and `catalogRouteRef`s from the `catalog-react` package +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- 8f0e8e039b: Removed usage of deprecated `getEntityMetadataViewUrl` and `getEntityMetadataEditUrl` helpers. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/plugin-catalog-common@0.2.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/search-common@0.3.0 + - @backstage/integration-react@0.1.24 + ## 0.9.0 ### Minor Changes diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index bd5edcbfa6..43cde23c57 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -7,25 +7,25 @@ import { ApiHolder } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { InfoCardVariants } from '@backstage/core-components'; +import { Observable } from '@backstage/types'; import { Overrides } from '@material-ui/core/styles/overrides'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; +import { StarredEntitiesApi } from '@backstage/plugin-catalog-react'; +import { StorageApi } from '@backstage/core-plugin-api'; import { StyleRules } from '@material-ui/core/styles/withStyles'; import { TableColumn } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; import { TabProps } from '@material-ui/core'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; -// @public @deprecated (undocumented) -export function AboutCard(props: AboutCardProps): JSX.Element; - // @public export interface AboutCardProps { // (undocumented) @@ -74,7 +74,7 @@ export function CatalogKindHeader(props: CatalogKindHeaderProps): JSX.Element; // @public export interface CatalogKindHeaderProps { - // (undocumented) + allowedKinds?: string[]; initialFilter?: string; } @@ -101,12 +101,6 @@ export const catalogPlugin: BackstagePlugin< } >; -// @public @deprecated (undocumented) -export const CatalogResultListItem: typeof CatalogSearchResultListItem; - -// @public @deprecated (undocumented) -export type CatalogResultListItemProps = CatalogSearchResultListItemProps; - // @public (undocumented) export function CatalogSearchResultListItem( props: CatalogSearchResultListItemProps, @@ -154,9 +148,9 @@ export interface CatalogTableRow { resolved: { name: string; partOfSystemRelationTitle?: string; - partOfSystemRelations: EntityName[]; + partOfSystemRelations: CompoundEntityRef[]; ownedByRelationsTitle?: string; - ownedByRelations: EntityName[]; + ownedByRelations: CompoundEntityRef[]; }; } @@ -170,6 +164,15 @@ export interface DefaultCatalogPageProps { initiallySelectedFilter?: UserListFilterKind; } +// @public +export class DefaultStarredEntitiesApi implements StarredEntitiesApi { + constructor(opts: { storageApi: StorageApi }); + // (undocumented) + starredEntitie$(): Observable>; + // (undocumented) + toggleStarred(entityRef: string): Promise; +} + // @public (undocumented) export interface DependencyOfComponentsCardProps { // (undocumented) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index f7800af139..971152a08d 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog", "description": "The Backstage plugin for browsing the Backstage catalog", - "version": "0.9.0", + "version": "0.10.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,16 +34,17 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-catalog-common": "^0.1.4", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/search-common": "^0.2.4", + "@backstage/integration-react": "^0.1.25-next.0", + "@backstage/plugin-catalog-common": "^0.2.2-next.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", + "@backstage/plugin-search-common": "^0.3.1-next.0", "@backstage/theme": "^0.2.15", + "@backstage/types": "^0.1.2", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -51,18 +52,19 @@ "lodash": "^4.17.21", "react-helmet": "6.1.0", "react-router": "6.0.0-beta.0", - "react-use": "^17.2.4" + "react-use": "^17.2.4", + "zen-observable": "^0.8.15" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/plugin-permission-react": "^0.3.2", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/plugin-permission-react": "^0.3.3", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts new file mode 100644 index 0000000000..2a3c4d83ed --- /dev/null +++ b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MockStorageApi } from '@backstage/test-utils'; +import { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi'; +import { performMigrationToTheNewBucket } from './migration'; + +jest.mock('./migration'); + +function getStarred(api: DefaultStarredEntitiesApi) { + return new Promise((resolve, reject) => { + const subscription = api.starredEntitie$().subscribe({ + next(starred) { + resolve(starred); + subscription.unsubscribe(); + }, + error: reject, + }); + }); +} + +describe('DefaultStarredEntitiesApi', () => { + beforeEach(() => { + (performMigrationToTheNewBucket as jest.Mock).mockResolvedValue(undefined); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('constructor', () => { + it('should call migration', () => { + const api = new DefaultStarredEntitiesApi({ + storageApi: MockStorageApi.create(), + }); + expect(performMigrationToTheNewBucket).toBeCalledTimes(1); + expect(api).toBeDefined(); + }); + }); + + it('should notify and toggle starred entities', async () => { + const entityRef = 'component:default/mock'; + + const storageApi = MockStorageApi.create(); + const storageBucket = storageApi.forBucket('starredEntities'); + const api = new DefaultStarredEntitiesApi({ storageApi }); + + const values = new Array>(); + api.starredEntitie$().subscribe({ + next: value => { + values.push(value); + }, + }); + + await expect(getStarred(api)).resolves.toEqual(new Set()); + + await api.toggleStarred(entityRef); + await expect(getStarred(api)).resolves.toEqual(new Set([entityRef])); + expect(storageBucket.snapshot('entityRefs')).toEqual( + expect.objectContaining({ presence: 'present', value: [entityRef] }), + ); + + await api.toggleStarred(entityRef); + await expect(getStarred(api)).resolves.toEqual(new Set()); + expect(storageBucket.snapshot('entityRefs')).toEqual( + expect.objectContaining({ presence: 'present', value: [] }), + ); + + expect(values).toEqual([new Set(), new Set([entityRef]), new Set()]); + }); + + it('should read starred entities from storage', async () => { + const entityRef = 'component:default/mock'; + + const storageApi = MockStorageApi.create(); + const storageBucket = storageApi.forBucket('starredEntities'); + storageBucket.set('entityRefs', [entityRef]); + const api = new DefaultStarredEntitiesApi({ storageApi }); + + await expect(getStarred(api)).resolves.toEqual(new Set([entityRef])); + }); +}); diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts similarity index 86% rename from plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts rename to plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts index 46a8e63084..f4b7771d04 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts @@ -15,10 +15,10 @@ */ import { StorageApi } from '@backstage/core-plugin-api'; +import { StarredEntitiesApi } from '@backstage/plugin-catalog-react'; import { Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; import { performMigrationToTheNewBucket } from './migration'; -import { StarredEntitiesApi } from './StarredEntitiesApi'; /** * Default implementation of the StarredEntitiesApi that is backed by the StorageApi. @@ -36,12 +36,12 @@ export class DefaultStarredEntitiesApi implements StarredEntitiesApi { this.settingsStore = opts.storageApi.forBucket('starredEntities'); this.starredEntities = new Set( - this.settingsStore.get('entityRefs') ?? [], + this.settingsStore.snapshot('entityRefs').value ?? [], ); this.settingsStore.observe$('entityRefs').subscribe({ next: next => { - this.starredEntities = new Set(next.newValue ?? []); + this.starredEntities = new Set(next.value ?? []); this.notifyChanges(); }, }); @@ -64,17 +64,13 @@ export class DefaultStarredEntitiesApi implements StarredEntitiesApi { return this.observable; } - isStarred(entityRef: string): boolean { - return this.starredEntities.has(entityRef); - } - private readonly subscribers = new Set< ZenObservable.SubscriptionObserver> >(); private readonly observable = new ObservableImpl>(subscriber => { // forward the the latest value - subscriber.next(this.starredEntities); + subscriber.next(new Set(this.starredEntities)); this.subscribers.add(subscriber); return () => { @@ -84,7 +80,7 @@ export class DefaultStarredEntitiesApi implements StarredEntitiesApi { private notifyChanges() { for (const subscription of this.subscribers) { - subscription.next(this.starredEntities); + subscription.next(new Set(this.starredEntities)); } } } diff --git a/plugins/catalog-backend/src/providers/index.ts b/plugins/catalog/src/apis/StarredEntitiesApi/index.ts similarity index 85% rename from plugins/catalog-backend/src/providers/index.ts rename to plugins/catalog/src/apis/StarredEntitiesApi/index.ts index 3b4a1c4b7e..42dc977fb6 100644 --- a/plugins/catalog-backend/src/providers/index.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/index.ts @@ -14,8 +14,4 @@ * limitations under the License. */ -export type { - EntityProvider, - EntityProviderConnection, - EntityProviderMutation, -} from './types'; +export { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi'; diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.test.ts b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts similarity index 81% rename from plugins/catalog-react/src/apis/StarredEntitiesApi/migration.test.ts rename to plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts index 7eb5648d13..b3bc5400d5 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.test.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts @@ -41,19 +41,19 @@ describe('performMigrationToTheNewBucket', () => { 'entity:Component:default:a', 'entity:template:custom:b', ]); - expect(oldBucket.get('starredEntities')).not.toBeUndefined(); + expect(oldBucket.snapshot('starredEntities').value).not.toBeUndefined(); await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(await newBucket.get('entityRefs')).toEqual([ + expect(await newBucket.snapshot('entityRefs').value).toEqual([ 'component:default/c', 'component:default/a', 'template:custom/b', ]); // OLD bucket should be removed - expect(oldBucket.get('starredEntities')).toBeUndefined(); + expect(oldBucket.snapshot('starredEntities').value).toBeUndefined(); }); it('should ignore invalid entries', async () => { @@ -67,15 +67,17 @@ describe('performMigrationToTheNewBucket', () => { 'entity:Component:a', 'invalid', ]); - expect(oldBucket.get('starredEntities')).not.toBeUndefined(); + expect(oldBucket.snapshot('starredEntities')).not.toBeUndefined(); await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(await newBucket.get('entityRefs')).toEqual(['component:default/a']); + expect(await newBucket.snapshot('entityRefs').value).toEqual([ + 'component:default/a', + ]); // OLD bucket should be removed - expect(oldBucket.get('starredEntities')).toBeUndefined(); + expect(oldBucket.snapshot('starredEntities').value).toBeUndefined(); }); it('should skip migration without old starred entities', async () => { @@ -88,7 +90,7 @@ describe('performMigrationToTheNewBucket', () => { await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(newBucket.get('entityRefs')).toEqual(expectedEntries); + expect(newBucket.snapshot('entityRefs').value).toEqual(expectedEntries); }); it('should skip migration with non-array old starred entities', async () => { @@ -105,9 +107,9 @@ describe('performMigrationToTheNewBucket', () => { await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(newBucket.get('entityRefs')).toEqual(expectedEntries); + expect(newBucket.snapshot('entityRefs').value).toEqual(expectedEntries); // OLD bucket should be unchanged - expect(oldBucket.get('starredEntities')).toBe('invalid'); + expect(oldBucket.snapshot('starredEntities').value).toBe('invalid'); }); }); diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts b/plugins/catalog/src/apis/StarredEntitiesApi/migration.ts similarity index 92% rename from plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts rename to plugins/catalog/src/apis/StarredEntitiesApi/migration.ts index 8405d9e08d..51dcd6e7ce 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/migration.ts @@ -35,13 +35,15 @@ export async function performMigrationToTheNewBucket({ const source = storageApi.forBucket('settings'); const target = storageApi.forBucket('starredEntities'); - const oldStarredEntities = source.get('starredEntities'); + const oldStarredEntities = source.snapshot('starredEntities').value; if (!isArray(oldStarredEntities)) { // nothing to do return; } - const targetEntities = new Set(target.get('entityRefs') ?? []); + const targetEntities = new Set( + target.snapshot('entityRefs').value ?? [], + ); oldStarredEntities .filter(isString) diff --git a/plugins/catalog-backend/src/ingestion/providers/index.ts b/plugins/catalog/src/apis/index.ts similarity index 89% rename from plugins/catalog-backend/src/ingestion/providers/index.ts rename to plugins/catalog/src/apis/index.ts index cd1bc5cb36..5c7e980890 100644 --- a/plugins/catalog-backend/src/ingestion/providers/index.ts +++ b/plugins/catalog/src/apis/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { GitHubOrgEntityProvider } from './GitHubOrgEntityProvider'; +export * from './StarredEntitiesApi'; diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 55cef5a5bd..b7ccd49b31 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -72,7 +72,7 @@ const useStyles = makeStyles({ }); /** - * Props for {@link AboutCard}. + * Props for {@link EntityAboutCard}. * * @public */ @@ -81,8 +81,7 @@ export interface AboutCardProps { } /** - * @public - * @deprecated Please use EntityAboutCard instead + * Exported publicly via the EntityAboutCard */ export function AboutCard(props: AboutCardProps) { const { variant } = props; diff --git a/plugins/catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts b/plugins/catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts index 605305882e..f4d06b0bb2 100644 --- a/plugins/catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts +++ b/plugins/catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts @@ -40,7 +40,7 @@ export const useEntityFromUrl = (): EntityLoadingStatus => { loading, retry: refresh, } = useAsyncRetry( - () => catalogApi.getEntityByName({ kind, namespace, name }), + () => catalogApi.getEntityByRef({ kind, namespace, name }), [catalogApi, kind, namespace, name], ); diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx index 45d42d0ea6..15d41f3ab3 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx @@ -135,4 +135,81 @@ describe('', () => { kind: new EntityKindFilter('template'), }); }); + + it('responds to external queryParameters changes', async () => { + const updateFilters = jest.fn(); + const rendered = await renderWithEffects( + + + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + kind: new EntityKindFilter('components'), + }); + rendered.rerender( + + + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + kind: new EntityKindFilter('template'), + }); + }); + + it('limits kinds when allowedKinds is set', async () => { + const rendered = await renderWithEffects( + + + + + , + ); + + const input = rendered.getByText('Components'); + fireEvent.mouseDown(input); + + expect( + rendered.getByRole('option', { name: 'Components' }), + ).toBeInTheDocument(); + expect( + rendered.getByRole('option', { name: 'Systems' }), + ).toBeInTheDocument(); + expect( + rendered.queryByRole('option', { name: 'Templates' }), + ).not.toBeInTheDocument(); + }); + + it('renders kind from the query parameter even when not in allowedKinds', async () => { + const rendered = await renderWithEffects( + + + + + , + ); + + expect(rendered.getByText('Frobs')).toBeInTheDocument(); + const input = rendered.getByText('Frobs'); + fireEvent.mouseDown(input); + + expect( + rendered.getByRole('option', { name: 'Systems' }), + ).toBeInTheDocument(); + }); }); diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx index ed54f1fa84..67eedb9acc 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { capitalize, createStyles, @@ -46,12 +46,21 @@ const useStyles = makeStyles((theme: Theme) => * @public */ export interface CatalogKindHeaderProps { + /** + * Entity kinds to show in the dropdown; by default all kinds are fetched from the catalog and + * displayed. + */ + allowedKinds?: string[]; + /** + * The initial kind to select; defaults to 'component'. A kind filter entered directly in the + * query parameter will override this value. + */ initialFilter?: string; } /** @public */ export function CatalogKindHeader(props: CatalogKindHeaderProps) { - const { initialFilter = 'component' } = props; + const { initialFilter = 'component', allowedKinds } = props; const classes = useStyles(); const catalogApi = useApi(catalogApiRef); const { value: allKinds } = useAsync(async () => { @@ -59,12 +68,17 @@ export function CatalogKindHeader(props: CatalogKindHeaderProps) { .getEntityFacets({ facets: ['kind'] }) .then(response => response.facets.kind?.map(f => f.value).sort() || []); }); - const { updateFilters, queryParameters } = useEntityList(); + const { + updateFilters, + queryParameters: { kind: kindParameter }, + } = useEntityList(); + const queryParamKind = useMemo( + () => [kindParameter].flat()[0]?.toLocaleLowerCase('en-US'), + [kindParameter], + ); const [selectedKind, setSelectedKind] = useState( - ([queryParameters.kind].flat()[0] ?? initialFilter).toLocaleLowerCase( - 'en-US', - ), + queryParamKind ?? initialFilter, ); useEffect(() => { @@ -73,18 +87,32 @@ export function CatalogKindHeader(props: CatalogKindHeaderProps) { }); }, [selectedKind, updateFilters]); + // Set selected Kind on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + if (queryParamKind) { + setSelectedKind(queryParamKind); + } + }, [queryParamKind]); + // Before allKinds is loaded, or when a kind is entered manually in the URL, selectedKind may not // be present in allKinds. It should still be shown in the dropdown, but may not have the nice // enforced casing from the catalog-backend. This makes a key/value record for the Select options, // including selectedKind if it's unknown - but allows the selectedKind to get clobbered by the // more proper catalog kind if it exists. - const options = [capitalize(selectedKind)] - .concat(allKinds ?? []) - .sort() - .reduce((acc, kind) => { - acc[kind.toLocaleLowerCase('en-US')] = kind; - return acc; - }, {} as Record); + const availableKinds = [capitalize(selectedKind)].concat( + allKinds?.filter(k => + allowedKinds + ? allowedKinds.some( + a => a.toLocaleLowerCase('en-US') === k.toLocaleLowerCase('en-US'), + ) + : true, + ) ?? [], + ); + const options = availableKinds.sort().reduce((acc, kind) => { + acc[kind.toLocaleLowerCase('en-US')] = kind; + return acc; + }, {} as Record); return ( ({ + label: e, + value: e, + }))} + onChange={el => setInstanceOption(el.toString())} + /> + ); + + const contentHeader = ( + + {sortingSelect} + + ); + + if (isNotFoundInInstance(aggregatedErrors)) { + return ( + + {contentHeader} + + + ); + } + + return ( + + {contentHeader} + + + ); +}; diff --git a/plugins/periskop/src/components/EntityPeriskopErrorsCard/index.ts b/plugins/periskop/src/components/EntityPeriskopErrorsCard/index.ts new file mode 100644 index 0000000000..0d0ab65d41 --- /dev/null +++ b/plugins/periskop/src/components/EntityPeriskopErrorsCard/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 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. + */ + +export { + EntityPeriskopErrorsCard, + isPeriskopAvailable, + PERISKOP_NAME_ANNOTATION, +} from './EntityPeriskopErrorsCard'; diff --git a/plugins/periskop/src/extensions.ts b/plugins/periskop/src/extensions.ts new file mode 100644 index 0000000000..990a41f24e --- /dev/null +++ b/plugins/periskop/src/extensions.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 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 { periskopPlugin } from './plugin'; +import { createComponentExtension } from '@backstage/core-plugin-api'; + +/** + * @public + */ +export const EntityPeriskopErrorsCard = periskopPlugin.provide( + createComponentExtension({ + name: 'EntityPeriskopErrorsCard', + component: { + lazy: () => + import('./components/EntityPeriskopErrorsCard').then( + m => m.EntityPeriskopErrorsCard, + ), + }, + }), +); diff --git a/plugins/periskop/src/index.ts b/plugins/periskop/src/index.ts new file mode 100644 index 0000000000..facb97bcc8 --- /dev/null +++ b/plugins/periskop/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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. + */ + +export { periskopPlugin, periskopApiRef } from './plugin'; +export { EntityPeriskopErrorsCard } from './extensions'; +export { + isPeriskopAvailable, + PERISKOP_NAME_ANNOTATION, +} from './components/EntityPeriskopErrorsCard'; +export * from './api/index'; +export * from './types'; diff --git a/plugins/periskop/src/plugin.test.ts b/plugins/periskop/src/plugin.test.ts new file mode 100644 index 0000000000..a921fa86e1 --- /dev/null +++ b/plugins/periskop/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 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 { periskopPlugin } from './plugin'; + +describe('periskop', () => { + it('should export plugin', () => { + expect(periskopPlugin).toBeDefined(); + }); +}); diff --git a/packages/catalog-client/src/types/deprecated.ts b/plugins/periskop/src/plugin.ts similarity index 51% rename from packages/catalog-client/src/types/deprecated.ts rename to plugins/periskop/src/plugin.ts index 0b145f3024..0fb0c2bede 100644 --- a/packages/catalog-client/src/types/deprecated.ts +++ b/plugins/periskop/src/plugin.ts @@ -15,29 +15,32 @@ */ import { - GetEntitiesRequest, - GetEntitiesResponse, - GetEntityAncestorsRequest, - GetEntityAncestorsResponse, -} from './api'; + discoveryApiRef, + configApiRef, + createApiFactory, + createPlugin, + createApiRef, +} from '@backstage/core-plugin-api'; +import { PeriskopApi, PeriskopClient } from './api'; /** * @public - * @deprecated use GetEntitiesRequest instead */ -export type CatalogEntitiesRequest = GetEntitiesRequest; +export const periskopApiRef = createApiRef({ + id: 'plugin.periskop.service', +}); + /** * @public - * @deprecated use GetEntitiesResponse instead */ -export type CatalogListResponse<_Entity> = GetEntitiesResponse; -/** - * @public - * @deprecated use GetEntityAncestorsRequest instead - */ -export type CatalogEntityAncestorsRequest = GetEntityAncestorsRequest; -/** - * @public - * @deprecated use GetEntityAncestorsResponse instead - */ -export type CatalogEntityAncestorsResponse = GetEntityAncestorsResponse; +export const periskopPlugin = createPlugin({ + id: 'periskop', + apis: [ + createApiFactory({ + api: periskopApiRef, + deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, + factory: ({ configApi, discoveryApi }) => + new PeriskopClient({ configApi, discoveryApi }), + }), + ], +}); diff --git a/plugins/periskop/src/routes.ts b/plugins/periskop/src/routes.ts new file mode 100644 index 0000000000..1a03f00498 --- /dev/null +++ b/plugins/periskop/src/routes.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 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 { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'periskop', +}); diff --git a/plugins/periskop/src/setupTests.ts b/plugins/periskop/src/setupTests.ts new file mode 100644 index 0000000000..2b5b615b19 --- /dev/null +++ b/plugins/periskop/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/periskop/src/types.ts b/plugins/periskop/src/types.ts new file mode 100644 index 0000000000..d3d0d6663f --- /dev/null +++ b/plugins/periskop/src/types.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2022 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. + */ + +/** @public */ +export interface AggregatedError { + aggregation_key: string; + total_count: number; + severity: string; + latest_errors: ErrorInstance[]; +} + +/** @public */ +export interface ErrorInstance { + error: Error; + uuid: string; + timestamp: number; + severity: string; + http_context: HttpContext; +} + +/** @public */ +export interface Error { + class: string; + message: string; + stacktrace?: string[]; + cause?: Error | null; +} + +/** @public */ +export interface HttpContext { + request_method: string; + request_url: string; + request_headers: RequestHeaders; + request_body: string; +} + +/** @public */ +export interface RequestHeaders { + [k: string]: string; +} + +/** @public */ +export interface NotFoundInInstance { + body: string; +} diff --git a/plugins/permission-backend/.eslintrc.js b/plugins/permission-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/permission-backend/.eslintrc.js +++ b/plugins/permission-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index 7ec949ff06..6964692c75 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-permission-backend +## 0.5.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-auth-node@0.1.5-next.0 + - @backstage/plugin-permission-node@0.5.4-next.0 + +## 0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-permission-node@0.5.3 + - @backstage/plugin-auth-node@0.1.4 + ## 0.5.2 ### Patch Changes diff --git a/plugins/permission-backend/api-report.md b/plugins/permission-backend/api-report.md index 2b59113535..3c606e3cb9 100644 --- a/plugins/permission-backend/api-report.md +++ b/plugins/permission-backend/api-report.md @@ -6,7 +6,7 @@ import { Config } from '@backstage/config'; import express from 'express'; import { IdentityClient } from '@backstage/plugin-auth-node'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PermissionPolicy } from '@backstage/plugin-permission-node'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -22,7 +22,7 @@ export interface RouterOptions { // (undocumented) identity: IdentityClient; // (undocumented) - logger: Logger_2; + logger: Logger; // (undocumented) policy: PermissionPolicy; } diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 30f8f45dd8..8b4c63ab3b 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.5.2", + "version": "0.5.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,12 +22,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/plugin-auth-node": "^0.1.3", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-permission-node": "^0.5.2", + "@backstage/plugin-auth-node": "^0.1.5-next.0", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-permission-node": "^0.5.4-next.0", "@types/express": "*", "dataloader": "^2.0.0", "express": "^4.17.1", @@ -39,7 +39,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", "supertest": "^6.1.6", diff --git a/plugins/permission-common/.eslintrc.js b/plugins/permission-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/permission-common/.eslintrc.js +++ b/plugins/permission-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/permission-common/CHANGELOG.md b/plugins/permission-common/CHANGELOG.md index b7162ecb06..bdeb000bd4 100644 --- a/plugins/permission-common/CHANGELOG.md +++ b/plugins/permission-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-permission-common +## 0.5.2 + +### Patch Changes + +- 79b9d8a861: Add api doc comments to `Permission` type properties. + ## 0.5.1 ### Patch Changes diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 122106da8f..b3147de3ee 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-common", "description": "Isomorphic types and client for Backstage permissions and authorization", - "version": "0.5.1", + "version": "0.5.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -48,7 +48,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.14.0", + "@backstage/cli": "^0.15.2-next.0", "@types/jest": "^26.0.7", "msw": "^0.35.0" } diff --git a/plugins/permission-common/src/types/permission.ts b/plugins/permission-common/src/types/permission.ts index 805aefa3de..16934d8771 100644 --- a/plugins/permission-common/src/types/permission.ts +++ b/plugins/permission-common/src/types/permission.ts @@ -37,8 +37,22 @@ export type PermissionAttributes = { * @public */ export type Permission = { + /** + * The name of the permission. + */ name: string; + /** + * {@link PermissionAttributes} which describe characteristics of the permission, to help + * policy authors make consistent decisions for similar permissions without referring to them + * all by name. + */ attributes: PermissionAttributes; + /** + * Some permissions can be authorized based on characteristics of a resource + * such a catalog entity. For these permissions, the resourceType field + * denotes the type of the resource whose resourceRef should be passed when + * authorizing. + */ resourceType?: string; }; diff --git a/plugins/permission-node/.eslintrc.js b/plugins/permission-node/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/permission-node/.eslintrc.js +++ b/plugins/permission-node/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index 74ea9b927b..72c192988c 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-permission-node +## 0.5.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-auth-node@0.1.5-next.0 + +## 0.5.3 + +### Patch Changes + +- 580f4e1df8: Export some utility functions for parsing PermissionCriteria + + `isAndCriteria`, `isOrCriteria`, `isNotCriteria` are now exported. + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-auth-node@0.1.4 + ## 0.5.2 ### Patch Changes diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 534ad45110..f6de7f4909 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -3,6 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AllOfCriteria } from '@backstage/plugin-permission-common'; +import { AnyOfCriteria } from '@backstage/plugin-permission-common'; import { AuthorizeDecision } from '@backstage/plugin-permission-common'; import { AuthorizeQuery } from '@backstage/plugin-permission-common'; import { AuthorizeRequestOptions } from '@backstage/plugin-permission-common'; @@ -11,6 +13,7 @@ import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { Config } from '@backstage/config'; import express from 'express'; import { Identified } from '@backstage/plugin-permission-common'; +import { NotCriteria } from '@backstage/plugin-permission-common'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { PermissionCondition } from '@backstage/plugin-permission-common'; import { PermissionCriteria } from '@backstage/plugin-permission-common'; @@ -118,6 +121,21 @@ export type DefinitivePolicyDecision = { result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; }; +// @alpha +export const isAndCriteria: ( + criteria: PermissionCriteria, +) => criteria is AllOfCriteria; + +// @alpha +export const isNotCriteria: ( + criteria: PermissionCriteria, +) => criteria is NotCriteria; + +// @alpha +export const isOrCriteria: ( + criteria: PermissionCriteria, +) => criteria is AnyOfCriteria; + // @public export const makeCreatePermissionRule: () => < TParams extends unknown[], diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 7fb5542c27..e514a7b8d4 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-node", "description": "Common permission and authorization utilities for backend plugins", - "version": "0.5.2", + "version": "0.5.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,18 +33,18 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/plugin-auth-node": "^0.1.3", - "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/plugin-auth-node": "^0.1.5-next.0", + "@backstage/plugin-permission-common": "^0.5.2", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/supertest": "^2.0.8", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/permission-node/src/integration/index.ts b/plugins/permission-node/src/integration/index.ts index 978342e4ed..7702fea95b 100644 --- a/plugins/permission-node/src/integration/index.ts +++ b/plugins/permission-node/src/integration/index.ts @@ -19,3 +19,4 @@ export * from './createConditionExports'; export * from './createConditionTransformer'; export * from './createPermissionIntegrationRouter'; export * from './createPermissionRule'; +export { isAndCriteria, isOrCriteria, isNotCriteria } from './util'; diff --git a/plugins/permission-node/src/integration/util.ts b/plugins/permission-node/src/integration/util.ts index 3878e18895..9102f092c4 100644 --- a/plugins/permission-node/src/integration/util.ts +++ b/plugins/permission-node/src/integration/util.ts @@ -22,20 +22,44 @@ import { } from '@backstage/plugin-permission-common'; import { PermissionRule } from '../types'; -export const isAndCriteria = ( - filter: PermissionCriteria, -): filter is AllOfCriteria => - Object.prototype.hasOwnProperty.call(filter, 'allOf'); +/** + * Utility function used to parse a PermissionCriteria + * @param criteria - a PermissionCriteria + * @alpha + * + * @returns `true` if the permission criteria is of type allOf, + * narrowing down `criteria` to the specific type. + */ +export const isAndCriteria = ( + criteria: PermissionCriteria, +): criteria is AllOfCriteria => + Object.prototype.hasOwnProperty.call(criteria, 'allOf'); -export const isOrCriteria = ( - filter: PermissionCriteria, -): filter is AnyOfCriteria => - Object.prototype.hasOwnProperty.call(filter, 'anyOf'); +/** + * Utility function used to parse a PermissionCriteria of type + * @param criteria - a PermissionCriteria + * @alpha + * + * @returns `true` if the permission criteria is of type anyOf, + * narrowing down `criteria` to the specific type. + */ +export const isOrCriteria = ( + criteria: PermissionCriteria, +): criteria is AnyOfCriteria => + Object.prototype.hasOwnProperty.call(criteria, 'anyOf'); -export const isNotCriteria = ( - filter: PermissionCriteria, -): filter is NotCriteria => - Object.prototype.hasOwnProperty.call(filter, 'not'); +/** + * Utility function used to parse a PermissionCriteria + * @param criteria - a PermissionCriteria + * @alpha + * + * @returns `true` if the permission criteria is of type not, + * narrowing down `criteria` to the specific type. + */ +export const isNotCriteria = ( + criteria: PermissionCriteria, +): criteria is NotCriteria => + Object.prototype.hasOwnProperty.call(criteria, 'not'); export const createGetRule = ( rules: PermissionRule[], diff --git a/plugins/permission-react/.eslintrc.js b/plugins/permission-react/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/permission-react/.eslintrc.js +++ b/plugins/permission-react/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index f83856d958..fa1e950899 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-permission-react +## 0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@0.8.0 + - @backstage/plugin-permission-common@0.5.2 + ## 0.3.2 ### Patch Changes diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index 9b338df822..936c6e0ff3 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-react", - "version": "0.3.2", + "version": "0.3.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-permission-common": "^0.5.2", "cross-fetch": "^3.1.5", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", @@ -44,8 +44,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@types/jest": "^26.0.7" diff --git a/plugins/proxy-backend/.eslintrc.js b/plugins/proxy-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/proxy-backend/.eslintrc.js +++ b/plugins/proxy-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index 84699fb11f..5ee0c417e4 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-proxy-backend +## 0.2.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + +## 0.2.22 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.2.21 ### Patch Changes diff --git a/plugins/proxy-backend/api-report.md b/plugins/proxy-backend/api-report.md index b71783cabf..45ef63f4d9 100644 --- a/plugins/proxy-backend/api-report.md +++ b/plugins/proxy-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; // Warning: (ae-forgotten-export) The symbol "RouterOptions" needs to be exported by the entry point index.d.ts diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 8bdb7b72bd..b28b95f664 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-proxy-backend", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", - "version": "0.2.21", + "version": "0.2.23-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -46,7 +46,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/rollbar-backend/.eslintrc.js b/plugins/rollbar-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/rollbar-backend/.eslintrc.js +++ b/plugins/rollbar-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index 0292160304..f8f91463b5 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-rollbar-backend +## 0.1.26-next.0 + +### Patch Changes + +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- 3c2bc73901: Use `setupRequestMockHandlers` from `@backstage/backend-test-utils` +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + +## 0.1.25 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.1.24 ### Patch Changes diff --git a/plugins/rollbar-backend/api-report.md b/plugins/rollbar-backend/api-report.md index d51292121f..1c0800a393 100644 --- a/plugins/rollbar-backend/api-report.md +++ b/plugins/rollbar-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -25,7 +25,7 @@ export function getRequestHeaders(token: string): { // // @public (undocumented) export class RollbarApi { - constructor(accessToken: string, logger: Logger_2); + constructor(accessToken: string, logger: Logger); // (undocumented) getActivatedCounts( projectName: string, @@ -107,7 +107,7 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; // (undocumented) rollbarApi?: RollbarApi; } diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 3f477e46d1..492f376b6c 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar-backend", "description": "A Backstage backend plugin that integrates towards Rollbar", - "version": "0.1.24", + "version": "0.1.26-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,7 +34,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@types/express": "^4.17.6", "camelcase-keys": "^7.0.1", @@ -42,7 +42,7 @@ "cors": "^2.8.5", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "fs-extra": "9.1.0", + "fs-extra": "10.0.1", "lodash": "^4.17.21", "morgan": "^1.10.0", "node-fetch": "^2.6.7", @@ -50,8 +50,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/backend-test-utils": "^0.1.21-next.0", + "@backstage/cli": "^0.15.2-next.0", "@types/supertest": "^2.0.8", "msw": "^0.36.3", "supertest": "^6.1.3" diff --git a/plugins/rollbar-backend/src/api/RollbarApi.test.ts b/plugins/rollbar-backend/src/api/RollbarApi.test.ts index 8936bb1839..430fc205c9 100644 --- a/plugins/rollbar-backend/src/api/RollbarApi.test.ts +++ b/plugins/rollbar-backend/src/api/RollbarApi.test.ts @@ -15,7 +15,7 @@ */ import { getRequestHeaders, RollbarApi } from './RollbarApi'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { getVoidLogger } from '@backstage/backend-common'; diff --git a/plugins/rollbar/.eslintrc.js b/plugins/rollbar/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/rollbar/.eslintrc.js +++ b/plugins/rollbar/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index d3dc72fc57..25d30be4f1 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-rollbar +## 0.4.2-next.0 + +### Patch Changes + +- 9844d4d2bd: Removed usage of removed hook. +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.4.1 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.4.0 ### Minor Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index acd07071e5..c2f81c8a89 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar", "description": "A Backstage plugin that integrates towards Rollbar", - "version": "0.4.0", + "version": "0.4.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,10 +35,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -53,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/rollbar/src/hooks/useCatalogEntity.ts b/plugins/rollbar/src/hooks/useCatalogEntity.ts index 468d0e8c50..4845a275be 100644 --- a/plugins/rollbar/src/hooks/useCatalogEntity.ts +++ b/plugins/rollbar/src/hooks/useCatalogEntity.ts @@ -14,23 +14,20 @@ * limitations under the License. */ -import { - catalogApiRef, - useEntityCompoundName, -} from '@backstage/plugin-catalog-react'; +import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import useAsync from 'react-use/lib/useAsync'; -import { useApi } from '@backstage/core-plugin-api'; +import { useApi, useRouteRefParams } from '@backstage/core-plugin-api'; export function useCatalogEntity() { const catalogApi = useApi(catalogApiRef); - const { namespace, name } = useEntityCompoundName(); + const { namespace, name } = useRouteRefParams(entityRouteRef); const { value: entity, error, loading, } = useAsync( - () => catalogApi.getEntityByName({ kind: 'Component', namespace, name }), + () => catalogApi.getEntityByRef({ kind: 'Component', namespace, name }), [catalogApi, namespace, name], ); diff --git a/plugins/scaffolder-backend-module-cookiecutter/.eslintrc.js b/plugins/scaffolder-backend-module-cookiecutter/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/.eslintrc.js +++ b/plugins/scaffolder-backend-module-cookiecutter/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 00e5f08ba4..890e6065cc 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.4-next.0 + +### Patch Changes + +- 8122e27717: Updating documentation for supporting `apiVersion: scaffolder.backstage.io/v1beta3` +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-scaffolder-backend@0.18.0-next.0 + +## 0.2.3 + +### Patch Changes + +- 6c7a879660: Fixed bug where existing cookiecutter.json file is not used. +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-scaffolder-backend@0.17.3 + - @backstage/integration@0.8.0 + ## 0.2.2 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/README.md b/plugins/scaffolder-backend-module-cookiecutter/README.md index 77ae93c5f3..f40200ee4d 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/README.md +++ b/plugins/scaffolder-backend-module-cookiecutter/README.md @@ -44,7 +44,7 @@ return await createRouter({ After that you can use the action in your template: ```yaml -apiVersion: backstage.io/v1beta2 +apiVersion: scaffolder.backstage.io/v1beta3 kind: Template metadata: name: cookiecutter-demo @@ -109,30 +109,31 @@ spec: input: url: ./template values: - name: '{{ parameters.name }}' - owner: '{{ parameters.owner }}' - system: '{{ parameters.system }}' - destination: '{{ parseRepoUrl parameters.repoUrl }}' + name: ${{ parameters.name }} + owner: ${{ parameters.owner }} + system: ${{ parameters.system }} + destination: ${{ parameters.repoUrl | parseRepoUrl }} - id: publish - if: '{{ not parameters.dryRun }}' + if: ${{ parameters.dryRun !== true }} name: Publish action: publish:github input: - allowedHosts: ['github.com'] - description: 'This is {{ parameters.name }}' - repoUrl: '{{ parameters.repoUrl }}' + allowedHosts: + - github.com + description: This is ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }} - id: register - if: '{{ not parameters.dryRun }}' + if: ${{ parameters.dryRun !== true }} name: Register action: catalog:register input: - repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' + repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }} catalogInfoPath: '/catalog-info.yaml' - name: Results - if: '{{ parameters.dryRun }}' + if: ${{ parameters.dryRun }} action: debug:log input: listWorkspace: true @@ -140,10 +141,10 @@ spec: output: links: - title: Repository - url: '{{ steps.publish.output.remoteUrl }}' + url: ${{ steps.publish.output.remoteUrl }} - title: Open in catalog - icon: 'catalog' - entityRef: '{{ steps.register.output.entityRef }}' + icon: catalog + entityRef: ${{ steps.register.output.entityRef }} ``` You can also visit the `/create/actions` route in your Backstage application to find out more about the parameters this action accepts when it's installed to configure how you like. diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 2e878fc882..6f04ab08a3 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.2.2", + "version": "0.2.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,19 +23,19 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/plugin-scaffolder-backend": "^0.17.0", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-scaffolder-backend": "^0.18.0-next.0", "@backstage/config": "^0.1.15", "@backstage/types": "^0.1.3", "command-exists": "^1.2.9", - "fs-extra": "10.0.0", + "fs-extra": "10.0.1", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/jest": "^26.0.7", diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index 6b0cb4695d..7575627506 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -57,23 +57,29 @@ export class CookiecutterRunner { workspacePath, values, logStream, + imageName, + templateDir, + templateContentsDir, }: { workspacePath: string; values: JsonObject; logStream: Writable; + imageName?: string; + templateDir: string; + templateContentsDir: string; }): Promise { - const templateDir = path.join(workspacePath, 'template'); const intermediateDir = path.join(workspacePath, 'intermediate'); await fs.ensureDir(intermediateDir); const resultDir = path.join(workspacePath, 'result'); // First lets grab the default cookiecutter.json file - const cookieCutterJson = await this.fetchTemplateCookieCutter(templateDir); + const cookieCutterJson = await this.fetchTemplateCookieCutter( + templateContentsDir, + ); - const { imageName, ...valuesForCookieCutterJson } = values; const cookieInfo = { ...cookieCutterJson, - ...valuesForCookieCutterJson, + ...values, }; await fs.writeJSON(path.join(templateDir, 'cookiecutter.json'), cookieInfo); @@ -96,7 +102,7 @@ export class CookiecutterRunner { }); } else { await this.containerRunner.runContainer({ - imageName: (imageName as string) ?? 'spotify/backstage-cookiecutter', + imageName: imageName ?? 'spotify/backstage-cookiecutter', command: 'cookiecutter', args: ['--no-input', '-o', '/output', '/input', '--verbose'], mountDirs, @@ -232,14 +238,16 @@ export function createFetchCookiecutterAction(options: { ...ctx.input.values, _copy_without_render: ctx.input.copyWithoutRender, _extensions: ctx.input.extensions, - imageName: ctx.input.imageName, }; // Will execute the template in ./template and put the result in ./result await cookiecutter.run({ workspacePath: workDir, logStream: ctx.logStream, - values, + values: values, + imageName: ctx.input.imageName, + templateDir: templateDir, + templateContentsDir: templateContentsDir, }); // Finally move the template result into the task workspace diff --git a/plugins/scaffolder-backend-module-rails/.eslintrc.js b/plugins/scaffolder-backend-module-rails/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/scaffolder-backend-module-rails/.eslintrc.js +++ b/plugins/scaffolder-backend-module-rails/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 5a71d8d8ec..7afc04c01a 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.3.4-next.0 + +### Patch Changes + +- 8122e27717: Updating documentation for supporting `apiVersion: scaffolder.backstage.io/v1beta3` +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-scaffolder-backend@0.18.0-next.0 + +## 0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-scaffolder-backend@0.17.3 + - @backstage/integration@0.8.0 + ## 0.3.2 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/README.md b/plugins/scaffolder-backend-module-rails/README.md index 4ac52be941..6236549c9d 100644 --- a/plugins/scaffolder-backend-module-rails/README.md +++ b/plugins/scaffolder-backend-module-rails/README.md @@ -45,7 +45,7 @@ return await createRouter({ After that you can use the action in your template: ```yaml -apiVersion: backstage.io/v1beta2 +apiVersion: scaffolder.backstage.io/v1beta3 kind: Template metadata: name: rails-demo @@ -171,10 +171,10 @@ spec: input: url: ./template values: - name: '{{ parameters.name }}' - owner: '{{ parameters.owner }}' - system: '{{ parameters.system }}' - railsArguments: '{{ json parameters.railsArguments }}' + name: ${{ parameters.name }} + owner: ${{ parameters.owner }} + system: ${{ parameters.system }} + railsArguments: ${{ parameters.railsArguments }} - name: Write Catalog information action: catalog:write @@ -183,33 +183,34 @@ spec: apiVersion: 'backstage.io/v1alpha1' kind: Component metadata: - name: '{{ parameters.name }}' + name: ${{ parameters.name }} annotations: - github.com/project-slug: '{{ projectSlug parameters.repoUrl }}' + github.com/project-slug: ${{ parameters.repoUrl | projectSlug }} spec: type: service lifecycle: production - owner: '{{ parameters.owner }}' + owner: ${{ parameters.owner }} - id: publish - if: '{{ not parameters.dryRun }}' + if: ${{ parameters.dryRun !== true }} name: Publish action: publish:github input: - allowedHosts: ['github.com'] - description: 'This is {{ parameters.name }}' - repoUrl: '{{ parameters.repoUrl }}' + allowedHosts: + - github.com + description: This is ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }} - id: register - if: '{{ not parameters.dryRun }}' + if: ${{ parameters.dryRun !== true }} name: Register action: catalog:register input: - repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' + repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }} catalogInfoPath: '/catalog-info.yaml' - name: Results - if: '{{ parameters.dryRun }}' + if: ${{ parameters.dryRun }} action: debug:log input: listWorkspace: true @@ -217,10 +218,10 @@ spec: output: links: - title: Repository - url: '{{ steps.publish.output.remoteUrl }}' + url: ${{ steps.publish.output.remoteUrl }} - title: Open in catalog - icon: 'catalog' - entityRef: '{{ steps.register.output.entityRef }}' + icon: catalog + entityRef: ${{ steps.register.output.entityRef }} ``` ### What you need to run that action @@ -240,8 +241,8 @@ steps: url: ./template imageName: repository/rails:tag values: - name: '{{ parameters.name }}' - owner: '{{ parameters.owner }}' - system: '{{ parameters.system }}' - railsArguments: '{{ json parameters.railsArguments }}' + name: ${{ parameters.name }} + owner: ${{ parameters.owner }} + system: ${{ parameters.system }} + railsArguments: ${{ parameters.railsArguments }} ``` diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index f3dc8cd8e5..053f48d1ce 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.3.2", + "version": "0.3.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,17 +24,17 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/plugin-scaffolder-backend": "^0.17.0", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/plugin-scaffolder-backend": "^0.18.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", + "@backstage/integration": "^0.8.0", "@backstage/types": "^0.1.3", "command-exists": "^1.2.9", - "fs-extra": "^9.0.0" + "fs-extra": "^10.0.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/command-exists": "^1.2.0", diff --git a/plugins/scaffolder-backend-module-yeoman/.eslintrc.js b/plugins/scaffolder-backend-module-yeoman/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/scaffolder-backend-module-yeoman/.eslintrc.js +++ b/plugins/scaffolder-backend-module-yeoman/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index f20f70b82e..0a7d6f06d5 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.2.2-next.0 + +### Patch Changes + +- 8122e27717: Updating documentation for supporting `apiVersion: scaffolder.backstage.io/v1beta3` +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.18.0-next.0 + ## 0.2.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/README.md b/plugins/scaffolder-backend-module-yeoman/README.md index bacbb87648..4cc57eac4a 100644 --- a/plugins/scaffolder-backend-module-yeoman/README.md +++ b/plugins/scaffolder-backend-module-yeoman/README.md @@ -44,7 +44,7 @@ return await createRouter({ After that you can use the action in your template: ```yaml -apiVersion: backstage.io/v1beta2 +apiVersion: scaffolder.backstage.io/v1beta3 kind: Template metadata: name: yeoman-demo @@ -107,29 +107,30 @@ spec: name: Yeoman action: run:yeoman input: - namespace: 'org:codeowners' + namespace: org:codeowners options: - codeowners: '@{{ parameters.owner }}' + codeowners: '@${{ parameters.owner }}' - id: publish - if: '{{ not parameters.dryRun }}' + if: ${{ parameters.dryRun !== true }} name: Publish action: publish:github input: - allowedHosts: ['github.com'] - description: 'This is {{ parameters.name }}' - repoUrl: '{{ parameters.repoUrl }}' + allowedHosts: + - github.com + description: This is ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }} - id: register - if: '{{ not parameters.dryRun }}' + if: ${{ parameters.dryRun !== true }} name: Register action: catalog:register input: - repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' + repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }} catalogInfoPath: '/catalog-info.yaml' - name: Results - if: '{{ parameters.dryRun }}' + if: ${{ parameters.dryRun }} action: debug:log input: listWorkspace: true @@ -137,10 +138,10 @@ spec: output: links: - title: Repository - url: '{{ steps.publish.output.remoteUrl }}' + url: ${{ steps.publish.output.remoteUrl }} - title: Open in catalog - icon: 'catalog' - entityRef: '{{ steps.register.output.entityRef }}' + icon: catalog + entityRef: ${{ steps.register.output.entityRef }} ``` You can also visit the `/create/actions` route in your Backstage application to find out more about the parameters this action accepts when it's installed to configure how you like. diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 609e8abab7..bc67774c59 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.2.1", + "version": "0.2.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,13 +24,13 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/plugin-scaffolder-backend": "^0.17.0", + "@backstage/plugin-scaffolder-backend": "^0.18.0-next.0", "@backstage/types": "^0.1.3", "winston": "^3.2.1", "yeoman-environment": "^3.9.1" }, "devDependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", "@types/jest": "^26.0.7" }, "files": [ diff --git a/plugins/scaffolder-backend/.eslintrc.js b/plugins/scaffolder-backend/.eslintrc.js index 2632b5e089..953af54f90 100644 --- a/plugins/scaffolder-backend/.eslintrc.js +++ b/plugins/scaffolder-backend/.eslintrc.js @@ -1,41 +1,18 @@ -const parent = require('@backstage/cli/config/eslint.backend'); - -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { ignorePatterns: ['sample-templates/'], - rules: { - 'no-console': 0, // Permitted in console programs - 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' - // Usage of path.resolve is extra sensitive in the scaffolder, so forbid it in non-test code - 'no-restricted-imports': [ - 'error', - { - ...parent.rules['no-restricted-imports'][1], - paths: [ - { - name: 'path', - importNames: ['resolve'], - message: - 'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues', - }, - ], - }, - ], - 'no-restricted-syntax': parent.rules['no-restricted-syntax'].concat([ - { - message: - 'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues', - selector: - 'MemberExpression[object.name="path"][property.name="resolve"]', - }, - ]), - }, - overrides: [ + restrictedSrcImports: [ { - files: ['*.test.*', 'src/setupTests.*', 'dev/**'], - rules: { - 'no-restricted-imports': parent.rules['no-restricted-imports'], - }, + name: 'path', + importNames: ['resolve'], + message: + 'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues', }, ], -}; + restrictedSrcSyntax: [ + { + message: + 'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues', + selector: 'MemberExpression[object.name="path"][property.name="resolve"]', + }, + ], +}); diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 25beb6c644..5e2ad656a4 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/plugin-scaffolder-backend +## 0.18.0-next.0 + +### Minor Changes + +- 310e905998: The following deprecations are now breaking and have been removed: + + - **BREAKING**: Support for `backstage.io/v1beta2` Software Templates has been removed. Please migrate your legacy templates to the new `scaffolder.backstage.io/v1beta3` `apiVersion` by following the [migration guide](https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3) + + - **BREAKING**: Removed the deprecated `TemplateMetadata`. Please use `TemplateInfo` instead. + + - **BREAKING**: Removed the deprecated `context.baseUrl`. It's now available on `context.templateInfo.baseUrl`. + + - **BREAKING**: Removed the deprecated `DispatchResult`, use `TaskBrokerDispatchResult` instead. + + - **BREAKING**: Removed the deprecated `runCommand`, use `executeShellCommond` instead. + + - **BREAKING**: Removed the deprecated `Status` in favour of `TaskStatus` instead. + + - **BREAKING**: Removed the deprecated `TaskState` in favour of `CurrentClaimedTask` instead. + +- f9c7bdd899: **BREAKING**: + + - Removed the `createFetchCookiecutterAction` export, please use the `@backstage/plugin-scaffolder-backend-module-cookiecutter` package explicitly (see [its README](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend-module-cookiecutter) for installation instructions). + - Removed the `containerRunner` argument from the types `RouterOptions` (as used by `createRouter`) and `CreateBuiltInActionsOptions` (as used by `createBuiltinActions`). + +- 5afbd16d43: **BREAKING**: Removed the previously deprecated `OctokitProvider` class. + +### Patch Changes + +- ab7cd7d70e: Do some groundwork for supporting the `better-sqlite3` driver, to maybe eventually replace `@vscode/sqlite3` (#9912) +- 8122e27717: Updating documentation for supporting `apiVersion: scaffolder.backstage.io/v1beta3` +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- 3c2bc73901: Use `setupRequestMockHandlers` from `@backstage/backend-test-utils` +- 458d16869c: Allow passing more repo configuration for `publish:github` action +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-catalog-backend@0.24.0-next.0 + - @backstage/plugin-scaffolder-common@0.3.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/catalog-client@0.9.0-next.0 + +## 0.17.3 + +### Patch Changes + +- 5c7f2343ea: Applied fix from version 0.17.2 of this package, which is part of the v0.69.2 release of Backstage. +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- 34af86517c: ensure `apiBaseUrl` being set for Bitbucket integrations, replace hardcoded defaults +- d6deb5e440: Set timeout for scaffolder octokit client +- 83a83381b0: Use the new `processingResult` export from the catalog backend +- 7372f29473: Cleanup API report +- c7f6424a26: Applied fix from `v0.17.1` of this package which is part of the `v0.69.1` release of Backstage. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- 8119a9e011: Fix the support for custom defaultBranch values for Bitbucket Cloud at the `publish:bitbucket` scaffolder action. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/plugin-catalog-backend@0.23.0 + - @backstage/backend-common@0.12.0 + - @backstage/integration@0.8.0 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.3 + - @backstage/plugin-scaffolder-common@0.2.3 + ## 0.17.2 ### Patch Changes diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 3bc03c4840..6934f2ef83 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -9,8 +9,6 @@ import { CatalogApi } from '@backstage/catalog-client'; import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; -import { ContainerRunner } from '@backstage/backend-common'; -import { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; import { createPullRequest } from 'octokit-plugin-create-pull-request'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; @@ -19,37 +17,31 @@ import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { Observable } from '@backstage/types'; -import { Octokit } from 'octokit'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; import { SpawnOptionsWithoutStdio } from 'child_process'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; -import { TemplateMetadata } from '@backstage/plugin-scaffolder-common'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; // @public export type ActionContext = { - baseUrl?: string; - logger: Logger_2; + logger: Logger; logStream: Writable; secrets?: TaskSecrets; workspacePath: string; input: Input; output(name: string, value: JsonValue): void; createTemporaryDirectory(): Promise; - metadata?: TemplateMetadata; templateInfo?: TemplateInfo; }; -// @public @deprecated -export type CompletedTaskState = TaskCompletionState; - // @public export const createBuiltinActions: ( options: CreateBuiltInActionsOptions, @@ -63,8 +55,6 @@ export interface CreateBuiltInActionsOptions { catalogClient: CatalogApi; // (undocumented) config: Config; - // @deprecated (undocumented) - containerRunner?: ContainerRunner; // (undocumented) integrations: ScmIntegrations; // (undocumented) @@ -99,8 +89,6 @@ export function createDebugLogAction(): TemplateAction<{ listWorkspace?: boolean | undefined; }>; -export { createFetchCookiecutterAction }; - // @public export function createFetchPlainAction(options: { reader: UrlReader; @@ -233,6 +221,10 @@ export function createPublishGithubAction(options: { description?: string | undefined; access?: string | undefined; defaultBranch?: string | undefined; + deleteBranchOnMerge?: boolean | undefined; + allowRebaseMerge?: boolean | undefined; + allowSquashMerge?: boolean | undefined; + allowMergeCommit?: boolean | undefined; sourcePath?: string | undefined; requireCodeOwnerReviews?: boolean | undefined; repoVisibility?: 'internal' | 'private' | 'public' | undefined; @@ -300,7 +292,7 @@ export type CreateWorkerOptions = { actionRegistry: TemplateActionRegistry; integrations: ScmIntegrations; workingDirectory: string; - logger: Logger_2; + logger: Logger; additionalTemplateFilters?: Record; }; @@ -363,9 +355,6 @@ export type DatabaseTaskStoreOptions = { database: Knex; }; -// @public @deprecated -export type DispatchResult = TaskBrokerDispatchResult; - // @public export const executeShellCommand: (options: RunCommandOptions) => Promise; @@ -384,25 +373,6 @@ export function fetchContents({ outputPath: string; }): Promise; -// Warning: (ae-missing-release-tag) "OctokitProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated -export class OctokitProvider { - constructor( - integrations: ScmIntegrationRegistry, - githubCredentialsProvider?: GithubCredentialsProvider, - ); - // Warning: (ae-forgotten-export) The symbol "OctokitIntegration" needs to be exported by the entry point index.d.ts - // - // @deprecated - getOctokit( - repoUrl: string, - options?: { - token?: string; - }, - ): Promise; -} - // @public (undocumented) export interface OctokitWithPullRequestPluginClient { // (undocumented) @@ -424,11 +394,9 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - containerRunner?: ContainerRunner; - // (undocumented) database: PluginDatabaseManager; // (undocumented) - logger: Logger_2; + logger: Logger; // (undocumented) reader: UrlReader; // (undocumented) @@ -437,9 +405,6 @@ export interface RouterOptions { taskWorkers?: number; } -// @public @deprecated -export const runCommand: (options: RunCommandOptions) => Promise; - // @public (undocumented) export type RunCommandOptions = { command: string; @@ -481,9 +446,6 @@ export type SerializedTaskEvent = { createdAt: string; }; -// @public @deprecated -export type Status = TaskStatus; - // @public export interface TaskBroker { // (undocumented) @@ -543,7 +505,7 @@ export class TaskManager implements TaskContext { static create( task: CurrentClaimedTask, storage: TaskStore, - logger: Logger_2, + logger: Logger, ): TaskManager; // (undocumented) get done(): boolean; @@ -554,7 +516,7 @@ export class TaskManager implements TaskContext { // (undocumented) get secrets(): TaskSecrets | undefined; // (undocumented) - get spec(): TaskSpec; + get spec(): TaskSpecV1beta3; } // @public @@ -562,9 +524,6 @@ export type TaskSecrets = Record & { backstageToken?: string; }; -// @public @deprecated -export type TaskState = CurrentClaimedTask; - // @public export type TaskStatus = | 'open' diff --git a/plugins/scaffolder-backend/migrations/20210120143715_init.js b/plugins/scaffolder-backend/migrations/20210120143715_init.js index fb53ce5b5e..08385a3dab 100644 --- a/plugins/scaffolder-backend/migrations/20210120143715_init.js +++ b/plugins/scaffolder-backend/migrations/20210120143715_init.js @@ -75,7 +75,7 @@ exports.up = async function up(knex) { * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { - if (knex.client.config.client !== 'sqlite3') { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('task_events', table => { table.dropIndex([], 'task_events_task_id_idx'); }); diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 51ff17c1e2..6ef03935e3 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "0.17.0", + "version": "0.18.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,15 +34,14 @@ "build:assets": "node scripts/build-nunjucks.js" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/plugin-catalog-backend": "^0.22.0", - "@backstage/plugin-scaffolder-common": "^0.2.2", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.2.2", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-catalog-backend": "^0.24.0-next.0", + "@backstage/plugin-scaffolder-common": "^0.3.0-next.0", "@backstage/types": "^0.1.3", "@gitbeaker/core": "^34.6.0", "@gitbeaker/node": "^35.1.0", @@ -54,10 +53,9 @@ "cors": "^2.8.5", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "fs-extra": "10.0.0", + "fs-extra": "10.0.1", "git-url-parse": "^11.6.0", "globby": "^11.0.0", - "handlebars": "^4.7.6", "isbinaryfile": "^4.0.8", "isomorphic-git": "^1.8.0", "jsonschema": "^1.2.6", @@ -76,8 +74,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/backend-test-utils": "^0.1.21-next.0", + "@backstage/cli": "^0.15.2-next.0", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml index 87f4d449f5..8b6ff302ac 100644 --- a/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml @@ -1,9 +1,9 @@ -apiVersion: backstage.io/v1beta2 +apiVersion: scaffolder.backstage.io/v1beta3 kind: Template metadata: name: bitbucket-demo title: Test Bitbucket RepoUrlPicker template - description: scaffolder v1beta2 template demo publishing to bitbucket + description: scaffolder v1beta3 template demo publishing to bitbucket spec: owner: backstage/techdocs-core type: service @@ -49,8 +49,8 @@ spec: input: url: ./template values: - name: '{{ parameters.name }}' - owner: '{{ parameters.owner }}' + name: ${{ parameters.name }} + owner: ${{ parameters.owner }} - id: fetch-docs name: Fetch Docs @@ -63,16 +63,20 @@ spec: name: Publish action: publish:bitbucket input: - description: 'This is {{ parameters.name }}' - repoUrl: '{{ parameters.repoUrl }}' + description: This is ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }} - id: register name: Register action: catalog:register input: - repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' + repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }} catalogInfoPath: '/catalog-info.yaml' output: - remoteUrl: '{{ steps.publish.output.remoteUrl }}' - entityRef: '{{ steps.register.output.entityRef }}' + links: + - title: Repository + url: ${{ steps.publish.output.remoteUrl }} + - title: Open in catalog + icon: catalog + entityRef: ${{ steps.register.output.entityRef }} diff --git a/plugins/scaffolder-backend/sample-templates/remote-templates.yaml b/plugins/scaffolder-backend/sample-templates/remote-templates.yaml index c5b2f1efea..a812c9ce52 100644 --- a/plugins/scaffolder-backend/sample-templates/remote-templates.yaml +++ b/plugins/scaffolder-backend/sample-templates/remote-templates.yaml @@ -11,4 +11,3 @@ spec: - https://github.com/backstage/software-templates/blob/main/scaffolder-templates/pull-request/template.yaml - https://github.com/backstage/software-templates/blob/main/scaffolder-templates/react-ssr-template/template.yaml - https://github.com/backstage/software-templates/blob/main/scaffolder-templates/springboot-grpc-template/template.yaml - - https://github.com/backstage/software-templates/blob/main/scaffolder-templates/v1beta2-demo/template.yaml diff --git a/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts index 43559fcd5f..d095b77b4c 100644 --- a/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts +++ b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts @@ -16,7 +16,7 @@ import { Entity, - getEntityName, + getCompoundEntityRef, parseEntityRef, RELATION_OWNED_BY, RELATION_OWNER_OF, @@ -25,7 +25,7 @@ import { CatalogProcessor, CatalogProcessorEmit, LocationSpec, - results, + processingResult, } from '@backstage/plugin-catalog-backend'; import { TemplateEntityV1beta3, @@ -55,7 +55,7 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { _location: LocationSpec, emit: CatalogProcessorEmit, ): Promise { - const selfRef = getEntityName(entity); + const selfRef = getCompoundEntityRef(entity); if ( entity.apiVersion === 'scaffolder.backstage.io/v1beta3' && @@ -70,7 +70,7 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { defaultNamespace: selfRef.namespace, }); emit( - results.relation({ + processingResult.relation({ source: selfRef, type: RELATION_OWNED_BY, target: { @@ -81,7 +81,7 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { }), ); emit( - results.relation({ + processingResult.relation({ source: { kind: targetRef.kind, namespace: targetRef.namespace, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index c03ba150c2..d7f15cdf13 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ContainerRunner, UrlReader } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; import { JsonObject } from '@backstage/types'; import { CatalogApi } from '@backstage/catalog-client'; import { @@ -30,7 +30,6 @@ import { import { createDebugLogAction } from './debug'; import { createFetchPlainAction, createFetchTemplateAction } from './fetch'; -import { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; import { createFilesystemDeleteAction, createFilesystemRenameAction, @@ -49,7 +48,6 @@ import { } from './github'; import { TemplateFilter } from '../../../lib'; import { TemplateAction } from '../types'; -import { getRootLogger } from '@backstage/backend-common'; /** * The options passed to {@link createBuiltinActions} @@ -59,8 +57,6 @@ export interface CreateBuiltInActionsOptions { reader: UrlReader; integrations: ScmIntegrations; catalogClient: CatalogApi; - /** @deprecated when the cookiecutter action is removed this won't be necessary */ - containerRunner?: ContainerRunner; config: Config; additionalTemplateFilters?: Record; } @@ -78,11 +74,11 @@ export const createBuiltinActions = ( const { reader, integrations, - containerRunner, catalogClient, config, additionalTemplateFilters, } = options; + const githubCredentialsProvider: GithubCredentialsProvider = DefaultGithubCredentialsProvider.fromIntegrations(integrations); @@ -135,20 +131,5 @@ export const createBuiltinActions = ( }), ]; - if (containerRunner) { - getRootLogger().warn( - `[DEPRECATED] The fetch:cookiecutter action will be removed part of the default scaffolder actions in later versions. -You can install the package seperately and remove the containerRunner from the createBuiltInActions to remove this warning, -or you can migrate to using fetch:template https://backstage.io/docs/features/software-templates/builtin-actions#migrating-from-fetchcookiecutter-to-fetchtemplate`, - ); - actions.push( - createFetchCookiecutterAction({ - reader, - integrations, - containerRunner, - }), - ); - } - return actions as TemplateAction[]; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.test.ts deleted file mode 100644 index e2449917ab..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { OctokitProvider } from './OctokitProvider'; -import { - ScmIntegrations, - DefaultGithubCredentialsProvider, -} from '@backstage/integration'; -import { ConfigReader } from '@backstage/config'; - -describe('getOctokit', () => { - const config = new ConfigReader({ - integrations: { - github: [ - { host: 'github.com', token: 'tokenlols' }, - { host: 'ghe.github.com' }, - ], - }, - }); - - const integrations = ScmIntegrations.fromConfig(config); - const githubCredentialsProvider = - DefaultGithubCredentialsProvider.fromIntegrations(integrations); - const octokitProvider = new OctokitProvider( - integrations, - githubCredentialsProvider, - ); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('should throw an error when the repoUrl is not well formed', async () => { - await expect( - octokitProvider.getOctokit('github.com?repo=bob'), - ).rejects.toThrow(/missing owner/); - - await expect( - octokitProvider.getOctokit('github.com?owner=owner'), - ).rejects.toThrow(/missing repo/); - }); - - it('should throw if there is no integration config provided', async () => { - await expect( - octokitProvider.getOctokit('missing.com?repo=bob&owner=owner'), - ).rejects.toThrow(/No matching integration configuration/); - }); - - it('should throw if there is no token in the integration config that is returned', async () => { - await expect( - octokitProvider.getOctokit('ghe.github.com?repo=bob&owner=owner'), - ).rejects.toThrow(/No token available for host/); - }); - - it('should return proper Octokit', async () => { - const { client, token, owner, repo } = await octokitProvider.getOctokit( - 'github.com?repo=bob&owner=owner', - ); - expect(client).toBeDefined(); - expect(token).toBe('tokenlols'); - expect(owner).toBe('owner'); - expect(repo).toBe('bob'); - }); - - it('should return an octokit client with the passed in token if it is provided', async () => { - const { client, token, owner, repo } = await octokitProvider.getOctokit( - 'github.com?repo=bob&owner=owner', - { token: 'tokenlols2' }, - ); - expect(client).toBeDefined(); - expect(token).toBe('tokenlols2'); - expect(owner).toBe('owner'); - expect(repo).toBe('bob'); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts deleted file mode 100644 index 2f3c43a6b8..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { InputError } from '@backstage/errors'; -import { - DefaultGithubCredentialsProvider, - GithubCredentialsProvider, - ScmIntegrationRegistry, -} from '@backstage/integration'; -import { Octokit } from 'octokit'; -import { parseRepoUrl } from '../publish/util'; - -export type OctokitIntegration = { - client: Octokit; - token: string; - owner: string; - repo: string; -}; -/** - * OctokitProvider provides Octokit client based on ScmIntegrationsRegistry configuration. - * OctokitProvider supports GitHub credentials caching out of the box. - * - * @deprecated we are no longer providing a way from the scaffolder to generate octokit instances. - * Implement your own if you're using this method from an external package, or use the internal `getOctokitOptions` function instead - */ -export class OctokitProvider { - private readonly integrations: ScmIntegrationRegistry; - private readonly githubCredentialsProvider: GithubCredentialsProvider; - - constructor( - integrations: ScmIntegrationRegistry, - githubCredentialsProvider?: GithubCredentialsProvider, - ) { - this.integrations = integrations; - this.githubCredentialsProvider = - githubCredentialsProvider || - DefaultGithubCredentialsProvider.fromIntegrations(this.integrations); - } - - /** - * gets standard Octokit client based on repository URL. - * - * @param repoUrl - Repository URL - * - * @deprecated we are no longer providing a way from the scaffolder to generate octokit instances. - * Implement your own if you're using this method from an external package, or use the internal `getOctokitOptions` function instead - */ - async getOctokit( - repoUrl: string, - options?: { token?: string }, - ): Promise { - const { owner, repo, host } = parseRepoUrl(repoUrl, this.integrations); - - if (!owner) { - throw new InputError(`No owner provided for repo ${repoUrl}`); - } - - const integrationConfig = this.integrations.github.byHost(host)?.config; - - if (!integrationConfig) { - throw new InputError(`No integration for host ${host}`); - } - - // Short circuit the internal Github Token provider the token provided - // by the action or the caller. - if (options?.token) { - const client = new Octokit({ - auth: options.token, - baseUrl: integrationConfig.apiBaseUrl, - previews: ['nebula-preview'], - }); - - return { client, token: options.token, owner, repo }; - } - - // TODO(blam): Consider changing this API to have owner, repoo interface instead of URL as the it's - // needless to create URL and then parse again the other side. - const { token } = await this.githubCredentialsProvider.getCredentials({ - url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent( - repo, - )}`, - }); - - if (!token) { - throw new InputError( - `No token available for host: ${host}, with owner ${owner}, and repo ${repo}`, - ); - } - - const client = new Octokit({ - auth: token, - baseUrl: integrationConfig.apiBaseUrl, - previews: ['nebula-preview'], - }); - - return { client, token, owner, repo }; - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts index 865c433c16..07614e8a03 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts @@ -16,4 +16,3 @@ export { createGithubActionsDispatchAction } from './githubActionsDispatch'; export { createGithubWebhookAction } from './githubWebhook'; -export { OctokitProvider } from './OctokitProvider'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 79537ff8dc..75e22522d5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -71,13 +71,6 @@ export const executeShellCommand = async (options: RunCommandOptions) => { }); }; -/** - * Run a command in a sub-process, normally a shell command. - * @public - * @deprecated use {@link executeShellCommand} instead - */ -export const runCommand = executeShellCommand; - export async function initRepoAndPush({ dir, remoteUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts index 8be1b62eaa..7846ff96fc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts @@ -22,8 +22,5 @@ export * from './filesystem'; export * from './publish'; export * from './github'; -/** @deprecated please add this package to your own installation manually */ -export { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; - -export { runCommand, executeShellCommand } from './helpers'; +export { executeShellCommand } from './helpers'; export type { RunCommandOptions } from './helpers'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index 96fd8184ca..ee989038c2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -19,7 +19,7 @@ jest.mock('../helpers'); import { createPublishBitbucketAction } from './bitbucket'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index 445a494234..665c54fb3e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -33,6 +33,7 @@ const createBitbucketCloudRepository = async (opts: { repoVisibility: 'private' | 'public'; mainBranch: string; authorization: string; + apiBaseUrl: string; }) => { const { workspace, @@ -42,6 +43,7 @@ const createBitbucketCloudRepository = async (opts: { repoVisibility, mainBranch, authorization, + apiBaseUrl, } = opts; const options: RequestInit = { @@ -61,7 +63,7 @@ const createBitbucketCloudRepository = async (opts: { let response: Response; try { response = await fetch( - `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo}`, + `${apiBaseUrl}/repositories/${workspace}/${repo}`, options, ); } catch (e) { @@ -91,16 +93,14 @@ const createBitbucketCloudRepository = async (opts: { }; const createBitbucketServerRepository = async (opts: { - host: string; project: string; repo: string; description?: string; repoVisibility: 'private' | 'public'; authorization: string; - apiBaseUrl?: string; + apiBaseUrl: string; }) => { const { - host, project, repo, description, @@ -124,8 +124,7 @@ const createBitbucketServerRepository = async (opts: { }; try { - const baseUrl = apiBaseUrl ? apiBaseUrl : `https://${host}/rest/api/1.0`; - response = await fetch(`${baseUrl}/projects/${project}/repos`, options); + response = await fetch(`${apiBaseUrl}/projects/${project}/repos`, options); } catch (e) { throw new Error(`Unable to create repository, ${e}`); } @@ -314,7 +313,11 @@ export function createPublishBitbucketAction(options: { const authorization = getAuthorizationHeader( ctx.input.token - ? { host: integrationConfig.config.host, token: ctx.input.token } + ? { + host: integrationConfig.config.host, + apiBaseUrl: integrationConfig.config.apiBaseUrl, + token: ctx.input.token, + } : integrationConfig.config, ); @@ -327,7 +330,6 @@ export function createPublishBitbucketAction(options: { const { remoteUrl, repoContentsUrl } = await createMethod({ authorization, - host, workspace: workspace || '', project, repo, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 1e58635bbe..ec564e93dc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -88,6 +88,10 @@ describe('publish:github', () => { name: 'repo', org: 'owner', private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, visibility: 'private', }); @@ -103,6 +107,10 @@ describe('publish:github', () => { name: 'repo', org: 'owner', private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, visibility: 'public', }); }); @@ -123,6 +131,10 @@ describe('publish:github', () => { description: 'description', name: 'repo', private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, }); await action.handler({ @@ -138,6 +150,10 @@ describe('publish:github', () => { description: 'description', name: 'repo', private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 926a63915a..7f1e7a1c93 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -46,6 +46,10 @@ export function createPublishGithubAction(options: { description?: string; access?: string; defaultBranch?: string; + deleteBranchOnMerge?: boolean; + allowRebaseMerge?: boolean; + allowSquashMerge?: boolean; + allowMergeCommit?: boolean; sourcePath?: string; requireCodeOwnerReviews?: boolean; repoVisibility?: 'private' | 'internal' | 'public'; @@ -94,6 +98,26 @@ export function createPublishGithubAction(options: { type: 'string', description: `Sets the default branch on the repository. The default value is 'master'`, }, + deleteBranchOnMerge: { + title: 'Delete Branch On Merge', + type: 'boolean', + description: `Delete the branch after merging the PR. The default value is 'false'`, + }, + allowMergeCommit: { + title: 'Allow Merge Commits', + type: 'boolean', + description: `Allow merge commits. The default value is 'true'`, + }, + allowSquashMerge: { + title: 'Allow Squash Merges', + type: 'boolean', + description: `Allow squash merges. The default value is 'true'`, + }, + allowRebaseMerge: { + title: 'Allow Rebase Merges', + type: 'boolean', + description: `Allow rebase merges. The default value is 'true'`, + }, sourcePath: { title: 'Source Path', description: @@ -156,6 +180,10 @@ export function createPublishGithubAction(options: { requireCodeOwnerReviews = false, repoVisibility = 'private', defaultBranch = 'master', + deleteBranchOnMerge = false, + allowMergeCommit = true, + allowSquashMerge = true, + allowRebaseMerge = true, collaborators, topics, token: providedToken, @@ -188,11 +216,19 @@ export function createPublishGithubAction(options: { private: repoVisibility === 'private', visibility: repoVisibility, description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + allow_rebase_merge: allowRebaseMerge, }) : client.rest.repos.createForAuthenticatedUser({ name: repo, private: repoVisibility === 'private', description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + allow_rebase_merge: allowRebaseMerge, }); const { data: newRepo } = await repoCreationPromise; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts index 2a87f58fd8..9a4ae8b2bf 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts @@ -19,21 +19,13 @@ import { Writable } from 'stream'; import { JsonValue, JsonObject } from '@backstage/types'; import { Schema } from 'jsonschema'; import { TaskSecrets } from '../tasks/types'; -import { - TemplateInfo, - TemplateMetadata, -} from '@backstage/plugin-scaffolder-common'; +import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; /** * ActionContext is passed into scaffolder actions. - * @public */ + * @public + */ export type ActionContext = { - /** - * Base URL for the location of the task spec, typically the url of the source entity file. - * @deprecated please use templateInfo.baseUrl instead - */ - baseUrl?: string; - logger: Logger; logStream: Writable; secrets?: TaskSecrets; @@ -46,10 +38,6 @@ export type ActionContext = { */ createTemporaryDirectory(): Promise; - /** - * @deprecated please use templateInfo instead - */ - metadata?: TemplateMetadata; templateInfo?: TemplateInfo; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 65b890c265..9fef68529f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -182,7 +182,7 @@ export class DatabaseTaskStore implements TaskStore { .andWhere( 'last_heartbeat_at', '<=', - this.db.client.config.client === 'sqlite3' + this.db.client.config.client.includes('sqlite3') ? this.db.raw(`datetime('now', ?)`, [`-${timeoutS} seconds`]) : this.db.raw(`dateadd('second', ?, ?)`, [ `-${timeoutS}`, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts deleted file mode 100644 index dcad1fc5d0..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts +++ /dev/null @@ -1,441 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import mockFs from 'mock-fs'; -import * as winston from 'winston'; -import { createTemplateAction, TemplateActionRegistry } from '../actions'; -import { ScmIntegrations } from '@backstage/integration'; -import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; -import { TaskContext } from './types'; -import { RepoSpec } from '../actions/builtin/publish/util'; -import { TaskSpec } from '@backstage/plugin-scaffolder-common'; - -describe('LegacyWorkflowRunner', () => { - let runner: HandlebarsWorkflowRunner; - const logger = getVoidLogger(); - let actionRegistry = new TemplateActionRegistry(); - let fakeActionHandler: jest.Mock; - - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - github: [{ host: 'github.com', token: 'token' }], - }, - }), - ); - - const createMockTaskWithSpec = (spec: TaskSpec): TaskContext => ({ - spec, - complete: async () => {}, - done: false, - emitLog: async () => {}, - getWorkspaceName: () => Promise.resolve('test-workspace'), - }); - - beforeEach(() => { - winston.format.simple(); // put logform the require cache before mocking fs - mockFs({ - '/tmp': mockFs.directory(), - }); - - actionRegistry = new TemplateActionRegistry(); - actionRegistry.register({ - id: 'test-action', - handler: async ctx => { - ctx.output('testOutput', 'mockOutputData'); - ctx.output('badOutput', false); - }, - }); - fakeActionHandler = jest.fn(); - actionRegistry.register({ - id: 'test-metadata-action', - handler: fakeActionHandler, - }); - - runner = new HandlebarsWorkflowRunner({ - actionRegistry, - integrations, - workingDirectory: '/tmp', - logger, - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it('should fail when the action does not exist', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], - output: { - result: '{{ steps.test.output.testOutput }}', - }, - values: {}, - }); - - await expect(() => runner.execute(task)).rejects.toThrow( - /Template action with ID 'not-found-action' is not registered/, - ); - }); - - it('should pass metadata through', async () => { - const entityRef = `template:default/templateName`; - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { - id: 'test', - name: 'name', - action: 'test-metadata-action', - input: { foo: 1 }, - }, - ], - output: {}, - values: {}, - templateInfo: { entityRef }, - }); - - await runner.execute(task); - - expect(fakeActionHandler.mock.calls[0][0].templateInfo).toEqual({ - entityRef, - }); - }); - - describe('templating', () => { - it('should template the output', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [{ id: 'test', name: 'test', action: 'test-action' }], - output: { - result: '{{ steps.test.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('mockOutputData'); - }); - - it('should template the input', async () => { - const inputAction = createTemplateAction<{ - name: string; - }>({ - id: 'test-input', - schema: { - input: { - type: 'object', - required: ['name'], - properties: { - name: { - title: 'name', - description: 'Enter name', - type: 'string', - }, - }, - }, - }, - async handler(ctx) { - if (ctx.input.name !== 'mockOutputData') { - throw new Error( - `expected name to be "mockOutputData" got ${ctx.input.name}`, - ); - } - }, - }); - actionRegistry.register(inputAction); - - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { id: 'test', name: 'test', action: 'test-action' }, - { - id: 'test-input', - name: 'test-input', - action: 'test-input', - input: { - name: '{{ steps.test.output.testOutput }}', - }, - }, - ], - output: { - result: '{{ steps.test.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('mockOutputData'); - }); - }); - - describe('conditionals', () => { - it('should execute steps conditionally', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { id: 'test', name: 'test', action: 'test-action' }, - { - id: 'conditional', - name: 'conditional', - action: 'test-action', - if: '{{ steps.test.output.testOutput }}', - }, - ], - output: { - result: '{{ steps.conditional.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('mockOutputData'); - }); - - it('should execute steps conditionally with eq helper', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { id: 'test', name: 'test', action: 'test-action' }, - { - id: 'conditional', - name: 'conditional', - action: 'test-action', - if: '{{ eq steps.test.output.testOutput "mockOutputData" }}', - }, - ], - output: { - result: '{{ steps.conditional.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('mockOutputData'); - }); - - it('should skip test conditionally', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { id: 'test', name: 'test', action: 'test-action' }, - { - id: 'conditional', - name: 'conditional', - action: 'test-action', - if: '{{ steps.test.output.badOutput }}', - }, - ], - output: { - result: '{{ steps.conditional.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBeUndefined(); - }); - }); - - describe('parsing', () => { - it('should parse strings as objects if possible', async () => { - const inputAction = createTemplateAction<{ - address: { line1: string }; - list: string[]; - address2: string; - }>({ - id: 'test-input', - schema: { - input: { - type: 'object', - required: ['address'], - properties: { - address: { - title: 'address', - description: 'Enter name', - type: 'object', - properties: { - line1: { - type: 'string', - }, - }, - }, - address2: { - type: 'string', - }, - list: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - }, - }, - async handler(ctx) { - if (ctx.input.list.length !== 1) { - throw new Error( - `expected list to have length "1" got ${ctx.input.list.length}`, - ); - } - if (ctx.input.address.line1 !== 'line 1') { - throw new Error( - `expected address.line1 to be "line 1" got ${ctx.input.address.line1}`, - ); - } - - if (ctx.input.address2 !== '{"not valid"}') { - throw new Error( - `expected address2 to be "{"not valid"}" got ${ctx.input.address2}`, - ); - } - ctx.output('address', ctx.input.address.line1); - }, - }); - actionRegistry.register(inputAction); - - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { - id: 'test-input', - name: 'test-input', - action: 'test-input', - input: { - address: JSON.stringify({ line1: 'line 1' }), - list: JSON.stringify(['hey!']), - address2: '{"not valid"}', - }, - }, - ], - output: { - result: '{{ steps.test-input.output.address }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('line 1'); - }); - - it('should provide a parseRepoUrl helper', async () => { - const inputAction = createTemplateAction<{ - destination: RepoSpec; - }>({ - id: 'test-input', - schema: { - input: { - type: 'object', - required: ['destination'], - properties: { - destination: { - title: 'destination', - type: 'object', - properties: { - repo: { - type: 'string', - }, - host: { - type: 'string', - }, - owner: { - type: 'string', - }, - organization: { - type: 'string', - }, - workspace: { - type: 'string', - }, - project: { - type: 'string', - }, - }, - }, - }, - }, - }, - async handler(ctx) { - ctx.output('host', ctx.input.destination.host); - ctx.output('repo', ctx.input.destination.repo); - - if (ctx.input.destination.owner) { - ctx.output('owner', ctx.input.destination.owner); - } - - if (ctx.input.destination.host !== 'github.com') { - throw new Error( - `expected host to be "github.com" got ${ctx.input.destination.host}`, - ); - } - - if (ctx.input.destination.repo !== 'repo') { - throw new Error( - `expected repo to be "repo" got ${ctx.input.destination.repo}`, - ); - } - - if ( - ctx.input.destination.owner && - ctx.input.destination.owner !== 'owner' - ) { - throw new Error( - `expected repo to be "owner" got ${ctx.input.destination.owner}`, - ); - } - }, - }); - actionRegistry.register(inputAction); - - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { - id: 'test-input', - name: 'test-input', - action: 'test-input', - input: { - destination: '{{ parseRepoUrl parameters.repoUrl }}', - }, - }, - ], - output: { - host: '{{ steps.test-input.output.host }}', - repo: '{{ steps.test-input.output.repo }}', - owner: '{{ steps.test-input.output.owner }}', - }, - values: { - repoUrl: 'github.com?repo=repo&owner=owner', - }, - }); - - const { output } = await runner.execute(task); - - expect(output.host).toBe('github.com'); - expect(output.repo).toBe('repo'); - expect(output.owner).toBe('owner'); - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts deleted file mode 100644 index c82da51ead..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { TaskContext, WorkflowRunner, WorkflowResponse } from './types'; -import * as Handlebars from 'handlebars'; -import { TemplateActionRegistry } from '..'; -import { ScmIntegrations } from '@backstage/integration'; -import { parseRepoUrl } from '../actions/builtin/publish/util'; -import { isTruthy } from './helper'; -import { PassThrough } from 'stream'; -import * as winston from 'winston'; -import { Logger } from 'winston'; -import path from 'path'; -import fs from 'fs-extra'; -import { validate as validateJsonSchema } from 'jsonschema'; -import { JsonObject, JsonValue } from '@backstage/types'; -import { InputError } from '@backstage/errors'; -import { TaskSpec, TaskSpecV1beta2 } from '@backstage/plugin-scaffolder-common'; - -type Options = { - workingDirectory: string; - actionRegistry: TemplateActionRegistry; - integrations: ScmIntegrations; - logger: Logger; -}; - -const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta2 => - taskSpec.apiVersion === 'backstage.io/v1beta2'; -/** - * This is the legacy workflow runner, which supports handlebars. This entire implementation will be replaced - * with the default workflow runner interface in the future so this entire thing can go bye bye. - */ -export class HandlebarsWorkflowRunner implements WorkflowRunner { - private readonly handlebars: typeof Handlebars; - - constructor(private readonly options: Options) { - this.handlebars = Handlebars.create(); - - // TODO(blam): this should be a public facing API but it's a little - // scary right now, so we're going to lock it off like the component API is - // in the frontend until we can work out a nice way to do it. - this.handlebars.registerHelper('parseRepoUrl', repoUrl => { - return JSON.stringify(parseRepoUrl(repoUrl, this.options.integrations)); - }); - - this.handlebars.registerHelper('projectSlug', repoUrl => { - const { owner, repo } = parseRepoUrl(repoUrl, this.options.integrations); - return `${owner}/${repo}`; - }); - - this.handlebars.registerHelper('json', obj => JSON.stringify(obj)); - - this.handlebars.registerHelper('not', value => !isTruthy(value)); - - this.handlebars.registerHelper('eq', (a, b) => a === b); - } - - async execute(task: TaskContext): Promise { - if (!isValidTaskSpec(task.spec)) { - throw new InputError(`Task spec is not a valid v1beta2 task spec`); - } - - const { actionRegistry } = this.options; - - const workspacePath = path.join( - this.options.workingDirectory, - await task.getWorkspaceName(), - ); - try { - await fs.ensureDir(workspacePath); - await task.emitLog( - `Starting up task with ${task.spec.steps.length} steps`, - ); - - const templateCtx: { - parameters: JsonObject; - steps: { - [stepName: string]: { output: { [outputName: string]: JsonValue } }; - }; - } = { parameters: task.spec.values, steps: {} }; - - for (const step of task.spec.steps) { - const metadata = { stepId: step.id }; - try { - const taskLogger = winston.createLogger({ - level: process.env.LOG_LEVEL || 'info', - format: winston.format.combine( - winston.format.colorize(), - winston.format.timestamp(), - winston.format.simple(), - ), - defaultMeta: {}, - }); - - const stream = new PassThrough(); - stream.on('data', async data => { - const message = data.toString().trim(); - if (message?.length > 1) { - await task.emitLog(message, metadata); - } - }); - - taskLogger.add(new winston.transports.Stream({ stream })); - - if (step.if !== undefined) { - // Support passing values like false to disable steps - let skip = !step.if; - - // Evaluate strings as handlebar templates - if (typeof step.if === 'string') { - const condition = JSON.parse( - JSON.stringify(step.if), - (_key, value) => { - if (typeof value === 'string') { - const templated = this.handlebars.compile(value, { - noEscape: true, - data: false, - preventIndent: true, - })(templateCtx); - - // If it's just an empty string, treat it as undefined - if (templated === '') { - return undefined; - } - - try { - return JSON.parse(templated); - } catch { - return templated; - } - } - - return value; - }, - ); - - skip = !isTruthy(condition); - } - - if (skip) { - await task.emitLog(`Skipped step ${step.name}`, { - ...metadata, - status: 'skipped', - }); - continue; - } - } - - await task.emitLog(`Beginning step ${step.name}`, { - ...metadata, - status: 'processing', - }); - - const action = actionRegistry.get(step.action); - if (!action) { - throw new Error(`Action '${step.action}' does not exist`); - } - - const input = - step.input && - JSON.parse(JSON.stringify(step.input), (_key, value) => { - if (typeof value === 'string') { - const templated = this.handlebars.compile(value, { - noEscape: true, - data: false, - preventIndent: true, - })(templateCtx); - - // If it smells like a JSON object then give it a parse as an object and if it fails return the string - if ( - (templated.startsWith('"') && templated.endsWith('"')) || - (templated.startsWith('{') && templated.endsWith('}')) || - (templated.startsWith('[') && templated.endsWith(']')) - ) { - try { - // Don't recursively JSON parse the values of this string. - // Shouldn't need to, don't want to encourage the use of returning handlebars from somewhere else - return JSON.parse(templated); - } catch { - return templated; - } - } - return templated; - } - - return value; - }); - - if (action.schema?.input) { - const validateResult = validateJsonSchema( - input, - action.schema.input, - ); - if (!validateResult.valid) { - const errors = validateResult.errors.join(', '); - throw new InputError( - `Invalid input passed to action ${action.id}, ${errors}`, - ); - } - } - - const stepOutputs: { [name: string]: JsonValue } = {}; - - // Keep track of all tmp dirs that are created by the action so we can remove them after - const tmpDirs = new Array(); - - this.options.logger.debug(`Running ${action.id} with input`, { - input: JSON.stringify(input, null, 2), - }); - - await action.handler({ - // deprecated in favor of templateInfo.baseUrl - baseUrl: task.spec.baseUrl, - logger: taskLogger, - logStream: stream, - input, - secrets: task.secrets ?? {}, - workspacePath, - async createTemporaryDirectory() { - const tmpDir = await fs.mkdtemp( - `${workspacePath}_step-${step.id}-`, - ); - tmpDirs.push(tmpDir); - return tmpDir; - }, - output(name: string, value: JsonValue) { - stepOutputs[name] = value; - }, - // deprecated in favor of templateInfo - metadata: task.spec.metadata, - templateInfo: task.spec.templateInfo, - }); - - // Remove all temporary directories that were created when executing the action - for (const tmpDir of tmpDirs) { - await fs.remove(tmpDir); - } - - templateCtx.steps[step.id] = { output: stepOutputs }; - - await task.emitLog(`Finished step ${step.name}`, { - ...metadata, - status: 'completed', - }); - } catch (error) { - await task.emitLog(String(error.stack), { - ...metadata, - status: 'failed', - }); - throw error; - } - } - - const output = JSON.parse( - JSON.stringify(task.spec.output), - (_key, value) => { - if (typeof value === 'string') { - const templated = this.handlebars.compile(value, { - noEscape: true, - data: false, - preventIndent: true, - })(templateCtx); - - // If it's just an empty string, treat it as undefined - if (templated === '') { - return undefined; - } - - // If it smells like a JSON object then give it a parse as an object and if it fails return the string - if ( - (templated.startsWith('"') && templated.endsWith('"')) || - (templated.startsWith('{') && templated.endsWith('}')) || - (templated.startsWith('[') && templated.endsWith(']')) - ) { - try { - // Don't recursively JSON parse the values of this string. - // Shouldn't need to, don't want to encourage the use of returning handlebars from somewhere else - return JSON.parse(templated); - } catch { - return templated; - } - } - return templated; - } - return value; - }, - ); - - return { output }; - } finally { - if (workspacePath) { - await fs.remove(workspacePath); - } - } - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index fb9da56c22..0bd5a3c840 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -257,8 +257,6 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { const stepOutput: { [outputName: string]: JsonValue } = {}; await action.handler({ - // deprecated in favourof templateInfo.baseUrl - baseUrl: task.spec.baseUrl, input, secrets: task.secrets ?? {}, logger: taskLogger, @@ -274,8 +272,6 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { output(name: string, value: JsonValue) { stepOutput[name] = value; }, - // deprecated in favour of templateInfo - metadata: task.spec.metadata, templateInfo: task.spec.templateInfo, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 0fd593f2f3..1399c25118 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -120,14 +120,6 @@ export interface CurrentClaimedTask { secrets?: TaskSecrets; } -/** - * TaskState - * - * @public - * @deprecated use CurrentClaimedTask instead - */ -export type TaskState = CurrentClaimedTask; - function defer() { let resolve = () => {}; const promise = new Promise(_resolve => { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index 9ff6b9c8a1..1a099e4c53 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -19,16 +19,10 @@ import { ConfigReader } from '@backstage/config'; import { DatabaseTaskStore } from './DatabaseTaskStore'; import { StorageTaskBroker } from './StorageTaskBroker'; import { TaskWorker } from './TaskWorker'; -import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateActionRegistry } from '../actions'; import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; -jest.mock('./HandlebarsWorkflowRunner'); -const MockedHandlebarsWorkflowRunner = - HandlebarsWorkflowRunner as jest.Mock; -MockedHandlebarsWorkflowRunner.mockImplementation(); - jest.mock('./NunjucksWorkflowRunner'); const MockedNunjucksWorkflowRunner = NunjucksWorkflowRunner as jest.Mock; @@ -58,10 +52,6 @@ describe('TaskWorker', () => { const actionRegistry: TemplateActionRegistry = {} as TemplateActionRegistry; const workingDirectory = '/tmp/scaffolder'; - const handlebarsWorkflowRunner: HandlebarsWorkflowRunner = { - execute: jest.fn(), - } as unknown as HandlebarsWorkflowRunner; - const workflowRunner: NunjucksWorkflowRunner = { execute: jest.fn(), } as unknown as NunjucksWorkflowRunner; @@ -72,47 +62,11 @@ describe('TaskWorker', () => { beforeEach(() => { jest.resetAllMocks(); - MockedHandlebarsWorkflowRunner.mockImplementation( - () => handlebarsWorkflowRunner, - ); MockedNunjucksWorkflowRunner.mockImplementation(() => workflowRunner); }); const logger = getVoidLogger(); - it('should call the legacy workflow runner when the apiVersion is not beta3', async () => { - const broker = new StorageTaskBroker(storage, logger); - const taskWorker = await TaskWorker.create({ - logger, - workingDirectory, - integrations, - taskBroker: broker, - actionRegistry, - }); - - await broker.dispatch({ - spec: { - apiVersion: 'backstage.io/v1beta2', - steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], - output: { - result: '{{ steps.test.output.testOutput }}', - }, - values: {}, - }, - }); - - const task = await broker.claim(); - await taskWorker.runOneTask(task); - - expect(MockedHandlebarsWorkflowRunner).toBeCalledWith({ - actionRegistry, - integrations, - logger, - workingDirectory, - }); - expect(handlebarsWorkflowRunner.execute).toHaveBeenCalled(); - }); - it('should call the default workflow runner when the apiVersion is beta3', async () => { const broker = new StorageTaskBroker(storage, logger); const taskWorker = await TaskWorker.create({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 8de9359806..9defd71c49 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -15,7 +15,6 @@ */ import { TaskContext, TaskBroker, WorkflowRunner } from './types'; -import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { Logger } from 'winston'; import { TemplateActionRegistry } from '../actions'; @@ -31,7 +30,6 @@ import { TemplateFilter } from '../../lib/templating/SecureTemplater'; export type TaskWorkerOptions = { taskBroker: TaskBroker; runners: { - legacyWorkflowRunner: HandlebarsWorkflowRunner; workflowRunner: WorkflowRunner; }; }; @@ -68,13 +66,6 @@ export class TaskWorker { additionalTemplateFilters, } = options; - const legacyWorkflowRunner = new HandlebarsWorkflowRunner({ - logger, - actionRegistry, - integrations, - workingDirectory, - }); - const workflowRunner = new NunjucksWorkflowRunner({ actionRegistry, integrations, @@ -85,7 +76,7 @@ export class TaskWorker { return new TaskWorker({ taskBroker: taskBroker, - runners: { legacyWorkflowRunner, workflowRunner }, + runners: { workflowRunner }, }); } @@ -100,10 +91,15 @@ export class TaskWorker { async runOneTask(task: TaskContext) { try { - const { output } = - task.spec.apiVersion === 'scaffolder.backstage.io/v1beta3' - ? await this.options.runners.workflowRunner.execute(task) - : await this.options.runners.legacyWorkflowRunner.execute(task); + if (task.spec.apiVersion !== 'scaffolder.backstage.io/v1beta3') { + throw new Error( + `Unsupported Template apiVersion ${task.spec.apiVersion}`, + ); + } + + const { output } = await this.options.runners.workflowRunner.execute( + task, + ); await task.complete('completed', { output }); } catch (error) { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index b108a3308d..457c32d9b5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -16,24 +16,21 @@ export { DatabaseTaskStore } from './DatabaseTaskStore'; export type { DatabaseTaskStoreOptions } from './DatabaseTaskStore'; export { TaskManager } from './StorageTaskBroker'; -export type { CurrentClaimedTask, TaskState } from './StorageTaskBroker'; +export type { CurrentClaimedTask } from './StorageTaskBroker'; export { TaskWorker } from './TaskWorker'; export type { CreateWorkerOptions } from './TaskWorker'; export type { TaskSecrets, TaskCompletionState, - CompletedTaskState, TaskStoreEmitOptions, TaskStoreListEventsOptions, SerializedTask, SerializedTaskEvent, - Status, TaskStatus, TaskEventType, TaskBroker, TaskContext, TaskStore, - DispatchResult, TaskBrokerDispatchResult, TaskBrokerDispatchOptions, TaskStoreCreateTaskOptions, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 57b92e0222..2af98b111b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -29,14 +29,6 @@ export type TaskStatus = | 'cancelled' | 'completed'; -/** - * The status of each step of the Task - * - * @public - * @deprecated use TaskStatus instead - */ -export type Status = TaskStatus; - /** * The state of a completed task. * @@ -44,14 +36,6 @@ export type Status = TaskStatus; */ export type TaskCompletionState = 'failed' | 'completed'; -/** - * The state of a completed task. - * - * @public - * @deprecated use TaskCompletionState instead - */ -export type CompletedTaskState = TaskCompletionState; - /** * SerializedTask * @@ -115,14 +99,6 @@ export type TaskBrokerDispatchOptions = { secrets?: TaskSecrets; }; -/** - * DispatchResult - * - * @public - * @deprecated use TaskBrokerDispatchResult instead - */ -export type DispatchResult = TaskBrokerDispatchResult; - /** * Task * diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts index e49c28cb74..23200d6aac 100644 --- a/plugins/scaffolder-backend/src/service/helpers.ts +++ b/plugins/scaffolder-backend/src/service/helpers.ts @@ -20,15 +20,13 @@ import { ANNOTATION_LOCATION, parseLocationRef, ANNOTATION_SOURCE_LOCATION, - EntityName, + CompoundEntityRef, DEFAULT_NAMESPACE, + stringifyEntityRef, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError, InputError, NotFoundError } from '@backstage/errors'; -import { - TemplateEntityV1beta2, - TemplateEntityV1beta3, -} from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import fs from 'fs-extra'; import os from 'os'; import { Logger } from 'winston'; @@ -91,10 +89,10 @@ export function getEntityBaseUrl(entity: Entity): string | undefined { * Returns the matching template, or throws a NotFoundError if no such template existed. */ export async function findTemplate(options: { - entityRef: EntityName; + entityRef: CompoundEntityRef; token?: string; catalogApi: CatalogApi; -}): Promise { +}): Promise { const { entityRef, token, catalogApi } = options; if (entityRef.namespace.toLocaleLowerCase('en-US') !== DEFAULT_NAMESPACE) { @@ -106,10 +104,12 @@ export async function findTemplate(options: { throw new InputError(`Invalid kind, only 'Template' kind is supported`); } - const template = await catalogApi.getEntityByName(entityRef, { token }); + const template = await catalogApi.getEntityByRef(entityRef, { token }); if (!template) { - throw new NotFoundError(`Template ${entityRef} not found`); + throw new NotFoundError( + `Template ${stringifyEntityRef(entityRef)} not found`, + ); } - return template as TemplateEntityV1beta3 | TemplateEntityV1beta2; + return template as TemplateEntityV1beta3; } diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index fa1b649231..41bc73c7ae 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -30,13 +30,12 @@ jest.doMock('fs-extra', () => ({ import { DatabaseManager, - DockerContainerRunner, getVoidLogger, PluginDatabaseManager, UrlReaders, } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ConfigReader } from '@backstage/config'; import ObservableImpl from 'zen-observable'; import express from 'express'; @@ -53,7 +52,7 @@ import { stringifyEntityRef } from '@backstage/catalog-model'; const createCatalogClient = (template: any) => ({ - getEntityByName: async () => template, + getEntityByRef: async () => template, } as unknown as CatalogApi); function createDatabase(): PluginDatabaseManager { @@ -77,8 +76,8 @@ const mockUrlReader = UrlReaders.default({ describe('createRouter', () => { let app: express.Express; let taskBroker: TaskBroker; - const template: TemplateEntityV1beta2 = { - apiVersion: 'backstage.io/v1beta2', + const template: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', metadata: { description: 'Create a new CRA website project', @@ -122,7 +121,6 @@ describe('createRouter', () => { config: new ConfigReader({}), database: createDatabase(), catalogClient: createCatalogClient(template), - containerRunner: new DockerContainerRunner({} as any), reader: mockUrlReader, taskBroker, }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 8f793a34d8..cf073b551b 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - ContainerRunner, - PluginDatabaseManager, - UrlReader, -} from '@backstage/backend-common'; +import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; @@ -26,11 +22,8 @@ import { Config } from '@backstage/config'; import { InputError, NotFoundError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import { - TemplateEntityV1beta2, TemplateEntityV1beta3, - TaskSpecV1beta3, TaskSpec, - TaskSpecV1beta2, } from '@backstage/plugin-scaffolder-common'; import express from 'express'; import Router from 'express-promise-router'; @@ -61,18 +54,12 @@ export interface RouterOptions { catalogClient: CatalogApi; actions?: TemplateAction[]; taskWorkers?: number; - containerRunner?: ContainerRunner; taskBroker?: TaskBroker; additionalTemplateFilters?: Record; } -function isSupportedTemplate( - entity: TemplateEntityV1beta2 | TemplateEntityV1beta3, -) { - return ( - entity.apiVersion === 'backstage.io/v1beta2' || - entity.apiVersion === 'scaffolder.backstage.io/v1beta3' - ); +function isSupportedTemplate(entity: TemplateEntityV1beta3) { + return entity.apiVersion === 'scaffolder.backstage.io/v1beta3'; } /** @public */ @@ -89,7 +76,6 @@ export async function createRouter( database, catalogClient, actions, - containerRunner, taskWorkers, additionalTemplateFilters, } = options; @@ -128,7 +114,6 @@ export async function createRouter( : createBuiltinActions({ integrations, catalogClient, - containerRunner, reader, config, additionalTemplateFilters, @@ -188,64 +173,7 @@ export async function createRouter( token: getBearerToken(req.headers.authorization), }); - let taskSpec: TaskSpec; - - if (isSupportedTemplate(template)) { - if (template.apiVersion === 'backstage.io/v1beta2') { - logger.warn( - `Scaffolding ${stringifyEntityRef( - template, - )} with deprecated apiVersion ${ - template.apiVersion - }. Please migrate the template to backstage.io/v1beta3. https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3`, - ); - } - - for (const parameters of [template.spec.parameters ?? []].flat()) { - const result = validate(values, parameters); - if (!result.valid) { - res.status(400).json({ errors: result.errors }); - return; - } - } - - const baseUrl = getEntityBaseUrl(template); - - const baseTaskSpec = { - baseUrl, - steps: template.spec.steps.map((step, index) => ({ - ...step, - id: step.id ?? `step-${index + 1}`, - name: step.name ?? step.action, - })), - output: template.spec.output ?? {}, - - // deprecated in favour of templateInfo - metadata: { name: template.metadata?.name }, - - templateInfo: { - entityRef: stringifyEntityRef({ - kind, - namespace, - name: template.metadata?.name, - }), - baseUrl, - }, - }; - - taskSpec = - template.apiVersion === 'backstage.io/v1beta2' - ? ({ - ...baseTaskSpec, - apiVersion: template.apiVersion, - values, - } as TaskSpecV1beta2) - : ({ - ...baseTaskSpec, - apiVersion: template.apiVersion, - parameters: values, - } as TaskSpecV1beta3); - } else { + if (!isSupportedTemplate(template)) { throw new InputError( `Unsupported apiVersion field in schema entity, ${ (template as Entity).apiVersion @@ -253,6 +181,35 @@ export async function createRouter( ); } + for (const parameters of [template.spec.parameters ?? []].flat()) { + const result = validate(values, parameters); + if (!result.valid) { + res.status(400).json({ errors: result.errors }); + return; + } + } + + const baseUrl = getEntityBaseUrl(template); + + const taskSpec: TaskSpec = { + apiVersion: template.apiVersion, + steps: template.spec.steps.map((step, index) => ({ + ...step, + id: step.id ?? `step-${index + 1}`, + name: step.name ?? step.action, + })), + output: template.spec.output ?? {}, + parameters: values, + templateInfo: { + entityRef: stringifyEntityRef({ + kind, + namespace, + name: template.metadata?.name, + }), + baseUrl, + }, + }; + const result = await taskBroker.dispatch({ spec: taskSpec, secrets: { diff --git a/plugins/scaffolder-common/.eslintrc.js b/plugins/scaffolder-common/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/scaffolder-common/.eslintrc.js +++ b/plugins/scaffolder-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index db702973fe..3c0ffabdb4 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-scaffolder-common +## 0.3.0-next.0 + +### Minor Changes + +- 310e905998: The following deprecations are now breaking and have been removed: + + - **BREAKING**: Support for `backstage.io/v1beta2` Software Templates has been removed. Please migrate your legacy templates to the new `scaffolder.backstage.io/v1beta3` `apiVersion` by following the [migration guide](https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3) + + - **BREAKING**: Removed the deprecated `TemplateMetadata`. Please use `TemplateInfo` instead. + + - **BREAKING**: Removed the deprecated `context.baseUrl`. It's now available on `context.templateInfo.baseUrl`. + + - **BREAKING**: Removed the deprecated `DispatchResult`, use `TaskBrokerDispatchResult` instead. + + - **BREAKING**: Removed the deprecated `runCommand`, use `executeShellCommond` instead. + + - **BREAKING**: Removed the deprecated `Status` in favour of `TaskStatus` instead. + + - **BREAKING**: Removed the deprecated `TaskState` in favour of `CurrentClaimedTask` instead. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.13.0-next.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + ## 0.2.2 ### Patch Changes diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md index 1872aa3ed8..6148440e6d 100644 --- a/plugins/scaffolder-common/api-report.md +++ b/plugins/scaffolder-common/api-report.md @@ -9,36 +9,12 @@ import { JsonValue } from '@backstage/types'; import { KindValidator } from '@backstage/catalog-model'; // @public -export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; - -// @public @deprecated -export interface TaskSpecV1beta2 { - // (undocumented) - apiVersion: 'backstage.io/v1beta2'; - // @deprecated (undocumented) - baseUrl?: string; - // @deprecated (undocumented) - metadata?: TemplateMetadata; - // (undocumented) - output: { - [name: string]: string; - }; - // (undocumented) - steps: TaskStep[]; - // (undocumented) - templateInfo?: TemplateInfo; - // (undocumented) - values: JsonObject; -} +export type TaskSpec = TaskSpecV1beta3; // @public export interface TaskSpecV1beta3 { // (undocumented) apiVersion: 'scaffolder.backstage.io/v1beta3'; - // @deprecated (undocumented) - baseUrl?: string; - // @deprecated (undocumented) - metadata?: TemplateMetadata; // (undocumented) output: { [name: string]: JsonValue; @@ -65,33 +41,6 @@ export interface TaskStep { name: string; } -// @public @deprecated -export interface TemplateEntityV1beta2 extends Entity { - // (undocumented) - apiVersion: 'backstage.io/v1beta2'; - // (undocumented) - kind: 'Template'; - // (undocumented) - spec: { - type: string; - parameters?: JsonObject | JsonObject[]; - steps: Array<{ - id?: string; - name?: string; - action: string; - input?: JsonObject; - if?: string | boolean; - }>; - output?: { - [name: string]: string; - }; - owner?: string; - }; -} - -// @public @deprecated -export const templateEntityV1beta2Validator: KindValidator; - // @public export interface TemplateEntityV1beta3 extends Entity { // (undocumented) @@ -124,9 +73,4 @@ export type TemplateInfo = { entityRef: string; baseUrl?: string; }; - -// @public @deprecated -export type TemplateMetadata = { - name: string; -}; ``` diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index be48edd743..596825cfe4 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-common", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", - "version": "0.2.2", + "version": "0.3.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,10 +39,10 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/types": "^0.1.3" }, "devDependencies": { - "@backstage/cli": "^0.14.1" + "@backstage/cli": "^0.15.2-next.0" } } diff --git a/plugins/scaffolder-common/src/TaskSpec.ts b/plugins/scaffolder-common/src/TaskSpec.ts index 2c7eb26d5b..326bb60ef6 100644 --- a/plugins/scaffolder-common/src/TaskSpec.ts +++ b/plugins/scaffolder-common/src/TaskSpec.ts @@ -16,17 +16,6 @@ import { JsonValue, JsonObject } from '@backstage/types'; -/** - * Metadata about the Template that was the originator of a scaffolder task, as - * stored in the database. - * - * @public - * @deprecated use templateInfo on the spec instead - */ -export type TemplateMetadata = { - name: string; -}; - /** * Information about a template that is stored on a task specification. * Includes a stringified entityRef, and the baseUrl which is usually the relative path of the template definition @@ -51,26 +40,6 @@ export interface TaskStep { if?: string | boolean; } -/** - * A scaffolder task as stored in the database, generated from a v1beta2 - * apiVersion Template. - * - * @public - * @deprecated Please convert your templates to TaskSpecV1beta3 on apiVersion - * scaffolder.backstage.io/v1beta3 - */ -export interface TaskSpecV1beta2 { - apiVersion: 'backstage.io/v1beta2'; - /** @deprecated use templateInfo.baseUrl instead */ - baseUrl?: string; - values: JsonObject; - steps: TaskStep[]; - output: { [name: string]: string }; - /** @deprecated use templateInfo instead */ - metadata?: TemplateMetadata; - templateInfo?: TemplateInfo; -} - /** * A scaffolder task as stored in the database, generated from a v1beta3 * apiVersion Template. @@ -79,13 +48,9 @@ export interface TaskSpecV1beta2 { */ export interface TaskSpecV1beta3 { apiVersion: 'scaffolder.backstage.io/v1beta3'; - /** @deprecated use templateInfo.baseUrl instead */ - baseUrl?: string; parameters: JsonObject; steps: TaskStep[]; output: { [name: string]: JsonValue }; - /** @deprecated use templateInfo instead */ - metadata?: TemplateMetadata; templateInfo?: TemplateInfo; } @@ -94,4 +59,4 @@ export interface TaskSpecV1beta3 { * * @public */ -export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; +export type TaskSpec = TaskSpecV1beta3; diff --git a/plugins/scaffolder-common/src/Template.v1beta2.schema.json b/plugins/scaffolder-common/src/Template.v1beta2.schema.json deleted file mode 100644 index d68eac936d..0000000000 --- a/plugins/scaffolder-common/src/Template.v1beta2.schema.json +++ /dev/null @@ -1,182 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "TemplateV1beta2", - "description": "A Template describes a scaffolding task for use with the Scaffolder. It describes the required parameters as well as a series of steps that will be taken to execute the scaffolding task.", - "examples": [ - { - "apiVersion": "backstage.io/v1beta2", - "kind": "Template", - "metadata": { - "name": "react-ssr-template", - "title": "React SSR Template", - "description": "Next.js application skeleton for creating isomorphic web applications.", - "tags": ["recommended", "react"] - }, - "spec": { - "owner": "artist-relations-team", - "type": "website", - "parameters": { - "required": ["name", "description"], - "properties": { - "name": { - "title": "Name", - "type": "string", - "description": "Unique name of the component" - }, - "description": { - "title": "Description", - "type": "string", - "description": "Description of the component" - } - } - }, - "steps": [ - { - "id": "fetch", - "name": "Fetch", - "action": "fetch:plain", - "parameters": { - "url": "./template" - } - }, - { - "id": "publish", - "name": "Publish to GitHub", - "action": "publish:github", - "parameters": { - "repoUrl": "{{ parameters.repoUrl }}" - }, - "if": "{{ parameters.repoUrl }}" - } - ], - "output": { - "catalogInfoUrl": "{{ steps.publish.output.catalogInfoUrl }}" - } - } - } - ], - "allOf": [ - { - "$ref": "Entity" - }, - { - "type": "object", - "required": ["spec"], - "properties": { - "apiVersion": { - "enum": ["backstage.io/v1beta2"] - }, - "kind": { - "enum": ["Template"] - }, - "spec": { - "type": "object", - "required": ["type", "steps"], - "properties": { - "type": { - "type": "string", - "description": "The type of component created by the template. The software catalog accepts any type value, but an organization should take great care to establish a proper taxonomy for these. Tools including Backstage itself may read this field and behave differently depending on its value. For example, a website type component may present tooling in the Backstage interface that is specific to just websites.", - "examples": ["service", "website", "library"], - "minLength": 1 - }, - "parameters": { - "oneOf": [ - { - "type": "object", - "description": "The JSONSchema describing the inputs for the template." - }, - { - "type": "array", - "description": "A list of separate forms to collect parameters.", - "items": { - "type": "object", - "description": "The JSONSchema describing the inputs for the template." - } - } - ] - }, - "steps": { - "type": "array", - "description": "A list of steps to execute.", - "items": { - "type": "object", - "description": "A description of the step to execute.", - "required": ["action"], - "properties": { - "id": { - "type": "string", - "description": "The ID of the step, which can be used to refer to its outputs." - }, - "name": { - "type": "string", - "description": "The name of the step, which will be displayed in the UI during the scaffolding process." - }, - "action": { - "type": "string", - "description": "The name of the action to execute." - }, - "input": { - "type": "object", - "description": "A templated object describing the inputs to the action." - }, - "if": { - "type": ["string", "boolean"], - "description": "A templated condition that skips the step when evaluated to false. If the condition is true or not defined, the step is executed. The condition is true, if the input is not `false`, `undefined`, `null`, `\"\"`, `0`, or `[]`." - } - } - } - }, - "output": { - "type": "object", - "description": "A templated object describing the outputs of the scaffolding task.", - "properties": { - "links": { - "type": "array", - "description": "A list of external hyperlinks, typically pointing to resources created or updated by the template", - "items": { - "type": "object", - "required": [], - "properties": { - "url": { - "type": "string", - "description": "A url in a standard uri format.", - "examples": ["https://github.com/my-org/my-new-repo"], - "minLength": 1 - }, - "entityRef": { - "type": "string", - "description": "An entity reference to an entity in the catalog.", - "examples": ["Component:default/my-app"], - "minLength": 1 - }, - "title": { - "type": "string", - "description": "A user friendly display name for the link.", - "examples": ["View new repo"], - "minLength": 1 - }, - "icon": { - "type": "string", - "description": "A key representing a visual icon to be displayed in the UI.", - "examples": ["dashboard"], - "minLength": 1 - } - } - } - } - }, - "additionalProperties": { - "type": "string" - } - }, - "owner": { - "type": "string", - "description": "The user (or group) owner of the template", - "minLength": 1 - } - } - } - } - } - ] -} diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta2.test.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta2.test.ts deleted file mode 100644 index f715f3c2b6..0000000000 --- a/plugins/scaffolder-common/src/TemplateEntityV1beta2.test.ts +++ /dev/null @@ -1,161 +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 { - TemplateEntityV1beta2, - templateEntityV1beta2Validator as validator, -} from './TemplateEntityV1beta2'; - -describe('templateEntityV1beta2Validator', () => { - let entity: TemplateEntityV1beta2; - - beforeEach(() => { - entity = { - apiVersion: 'backstage.io/v1beta2', - kind: 'Template', - metadata: { - name: 'test', - }, - spec: { - type: 'website', - parameters: { - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - steps: [ - { - id: 'fetch', - name: 'Fetch', - action: 'fetch:plan', - input: { - url: './template', - }, - if: '{{ parameters.owner }}', - }, - ], - output: { - fetchUrl: '{{ steps.fetch.output.targetUrl }}', - }, - owner: 'team-b@example.com', - }, - }; - }); - - it('happy path: accepts valid data', async () => { - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('ignores unknown apiVersion', async () => { - (entity as any).apiVersion = 'backstage.io/v1beta0'; - await expect(validator.check(entity)).resolves.toBe(false); - }); - - it('ignores unknown kind', async () => { - (entity as any).kind = 'Wizard'; - await expect(validator.check(entity)).resolves.toBe(false); - }); - - it('rejects missing type', async () => { - delete (entity as any).spec.type; - await expect(validator.check(entity)).rejects.toThrow(/type/); - }); - - it('accepts any other type', async () => { - (entity as any).spec.type = 'hallo'; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('accepts missing parameters', async () => { - delete (entity as any).spec.parameters; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('accepts missing outputs', async () => { - delete (entity as any).spec.outputs; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('rejects empty type', async () => { - (entity as any).spec.type = ''; - await expect(validator.check(entity)).rejects.toThrow(/type/); - }); - - it('rejects missing steps', async () => { - delete (entity as any).spec.steps; - await expect(validator.check(entity)).rejects.toThrow(/steps/); - }); - - it('accepts step with missing id', async () => { - delete (entity as any).spec.steps[0].id; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('accepts step with missing name', async () => { - delete (entity as any).spec.steps[0].name; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('rejects step with missing action', async () => { - delete (entity as any).spec.steps[0].action; - await expect(validator.check(entity)).rejects.toThrow(/action/); - }); - - it('accepts missing owner', async () => { - delete (entity as any).spec.owner; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('rejects empty owner', async () => { - (entity as any).spec.owner = ''; - await expect(validator.check(entity)).rejects.toThrow(/owner/); - }); - - it('rejects wrong type owner', async () => { - (entity as any).spec.owner = 5; - await expect(validator.check(entity)).rejects.toThrow(/owner/); - }); - - it('accepts missing if', async () => { - delete (entity as any).spec.steps[0].if; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('accepts boolean in if', async () => { - (entity as any).spec.steps[0].if = true; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('accepts empty if', async () => { - (entity as any).spec.steps[0].if = ''; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('rejects wrong type if', async () => { - (entity as any).spec.steps[0].if = 5; - await expect(validator.check(entity)).rejects.toThrow(/if/); - }); -}); diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta2.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta2.ts deleted file mode 100644 index 280caec52f..0000000000 --- a/plugins/scaffolder-common/src/TemplateEntityV1beta2.ts +++ /dev/null @@ -1,65 +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 { - Entity, - entityKindSchemaValidator, - KindValidator, -} from '@backstage/catalog-model'; -import { JsonObject } from '@backstage/types'; -import schema from './Template.v1beta2.schema.json'; - -/** - * Backstage catalog Template kind Entity. Templates are used by the Scaffolder - * plugin to create new entities, such as Components. - * - * @public - * @deprecated Please convert your templates to TemplateEntityV1beta3 on - * apiVersion scaffolder.backstage.io/v1beta3 - */ -export interface TemplateEntityV1beta2 extends Entity { - apiVersion: 'backstage.io/v1beta2'; - kind: 'Template'; - spec: { - type: string; - parameters?: JsonObject | JsonObject[]; - steps: Array<{ - id?: string; - name?: string; - action: string; - input?: JsonObject; - if?: string | boolean; - }>; - output?: { [name: string]: string }; - owner?: string; - }; -} - -const validator = entityKindSchemaValidator(schema); - -/** - * Entity data validator for {@link TemplateEntityV1beta2}. - * - * @public - * @deprecated Please convert your templates to TemplateEntityV1beta3 on - * apiVersion scaffolder.backstage.io/v1beta3 - */ -export const templateEntityV1beta2Validator: KindValidator = { - // TODO(freben): Emulate the old KindValidator until we fix that type - async check(data: Entity) { - return validator(data) === data; - }, -}; diff --git a/plugins/scaffolder-common/src/index.ts b/plugins/scaffolder-common/src/index.ts index e74a5f988f..521e678ab6 100644 --- a/plugins/scaffolder-common/src/index.ts +++ b/plugins/scaffolder-common/src/index.ts @@ -21,7 +21,5 @@ */ export * from './TaskSpec'; -export { templateEntityV1beta2Validator } from './TemplateEntityV1beta2'; -export type { TemplateEntityV1beta2 } from './TemplateEntityV1beta2'; export { templateEntityV1beta3Validator } from './TemplateEntityV1beta3'; export type { TemplateEntityV1beta3 } from './TemplateEntityV1beta3'; diff --git a/plugins/scaffolder/.eslintrc.js b/plugins/scaffolder/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/scaffolder/.eslintrc.js +++ b/plugins/scaffolder/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 1d1cddfa31..0549fba214 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,81 @@ # @backstage/plugin-scaffolder +## 0.15.0-next.0 + +### Minor Changes + +- 310e905998: The following deprecations are now breaking and have been removed: + + - **BREAKING**: Support for `backstage.io/v1beta2` Software Templates has been removed. Please migrate your legacy templates to the new `scaffolder.backstage.io/v1beta3` `apiVersion` by following the [migration guide](https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3) + + - **BREAKING**: Removed the deprecated `TemplateMetadata`. Please use `TemplateInfo` instead. + + - **BREAKING**: Removed the deprecated `context.baseUrl`. It's now available on `context.templateInfo.baseUrl`. + + - **BREAKING**: Removed the deprecated `DispatchResult`, use `TaskBrokerDispatchResult` instead. + + - **BREAKING**: Removed the deprecated `runCommand`, use `executeShellCommond` instead. + + - **BREAKING**: Removed the deprecated `Status` in favour of `TaskStatus` instead. + + - **BREAKING**: Removed the deprecated `TaskState` in favour of `CurrentClaimedTask` instead. + +- 1360f7d73a: **BREAKING**: Removed `ScaffolderTaskOutput.entityRef` and `ScaffolderTaskOutput.remoteUrl`, which both have been deprecated for over a year. Please use the `links` output instead. +- e63e5a9452: Removed the following previously deprecated exports: + + - **BREAKING**: Removed the deprecated `TemplateList` component and the `TemplateListProps` type. Please use the `TemplateCard` to create your own list component instead to render these lists. + + - **BREAKING**: Removed the deprecated `setSecret` method, please use `setSecrets` instead. + + - **BREAKING**: Removed the deprecated `TemplateCardComponent` and `TaskPageComponent` props from the `ScaffolderPage` component. These are now provided using the `components` prop with the shape `{{ TemplateCardComponent: () => JSX.Element, TaskPageComponent: () => JSX.Element }}` + + - **BREAKING**: Removed `JobStatus` as this type was actually a legacy type used in `v1alpha` templates and the workflow engine and should no longer be used or depended on. + +### Patch Changes + +- d741c97b98: Render markdown for description in software templates +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/plugin-scaffolder-common@0.3.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/plugin-catalog-common@0.2.2-next.0 + - @backstage/catalog-client@0.9.0-next.0 + - @backstage/integration-react@0.1.25-next.0 + +## 0.14.0 + +### Minor Changes + +- 1c2755991d: - **BREAKING**: Removed the `FavouriteTemplate` export in favor of the `FavoriteEntity` from `@backstage/plugin-catalog-react`. Please migrate any usages to that component instead if you are creating your own `TemplateCard` page. +- 86da51cec5: **BREAKING**: Removing the exports of the raw components that back the `CustomFieldExtensions`. + +### Patch Changes + +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- 55361f3f7b: Added some deprecations as follows: + + - **DEPRECATED**: `TemplateCardComponent` and `TaskPageComponent` props have been deprecated, and moved to a `components` prop instead. You can pass them in through there instead. + - **DEPRECATED**: `TemplateList` and `TemplateListProps` has been deprecated. Please use the `TemplateCard` to create your own list component instead. + - **DEPRECATED**: `setSecret` has been deprecated in favour of `setSecrets` when calling `useTemplateSecrets` + + Other notable changes: + + - `scaffolderApi.scaffold()` `values` type has been narrowed from `Record` to `Record` instead. + - Moved all navigation internally over to using `routeRefs` and `subRouteRefs` + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/plugin-catalog-common@0.2.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/plugin-scaffolder-common@0.2.3 + - @backstage/integration-react@0.1.24 + - @backstage/plugin-permission-react@0.3.3 + ## 0.13.0 ### Minor Changes diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 82dfecfaa5..90fee1fc66 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -8,7 +8,6 @@ import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { ComponentProps } from 'react'; import { ComponentType } from 'react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; @@ -17,7 +16,6 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; import { FieldProps } from '@rjsf/core'; import { FieldValidation } from '@rjsf/core'; -import { IconButton } from '@material-ui/core'; import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; import { JsonValue } from '@backstage/types'; @@ -26,17 +24,15 @@ import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -// Warning: (ae-missing-release-tag) "createScaffolderFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createScaffolderFieldExtension< TReturnValue = unknown, TInputProps = unknown, >( options: FieldExtensionOptions, -): Extension<() => null>; +): Extension>; // @public export type CustomFieldValidator = ( @@ -47,26 +43,19 @@ export type CustomFieldValidator = ( }, ) => void; -// Warning: (ae-missing-release-tag) "EntityNamePickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const EntityNamePickerFieldExtension: () => null; - -// Warning: (ae-missing-release-tag) "EntityPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const EntityPicker: ( - props: FieldExtensionComponentProps, -) => JSX.Element; +export const EntityNamePickerFieldExtension: FieldExtensionComponent< + string, + {} +>; -// Warning: (ae-missing-release-tag) "EntityPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const EntityPickerFieldExtension: () => null; +// @public +export const EntityPickerFieldExtension: FieldExtensionComponent< + string, + EntityPickerUiOptions +>; -// Warning: (ae-missing-release-tag) "EntityPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface EntityPickerUiOptions { // (undocumented) allowArbitraryValues?: boolean; @@ -76,29 +65,20 @@ export interface EntityPickerUiOptions { defaultKind?: string; } -// Warning: (ae-missing-release-tag) "EntityTagsPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const EntityTagsPicker: ( - props: FieldExtensionComponentProps, -) => JSX.Element; +export const EntityTagsPickerFieldExtension: FieldExtensionComponent< + string[], + EntityTagsPickerUiOptions +>; // @public -export const EntityTagsPickerFieldExtension: () => null; - -// Warning: (ae-missing-release-tag) "EntityTagsPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) export interface EntityTagsPickerUiOptions { // (undocumented) kinds?: string[]; } -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "FavouriteTemplate" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const FavouriteTemplate: (props: Props) => JSX.Element; +export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; // @public export interface FieldExtensionComponentProps< @@ -114,21 +94,16 @@ export interface FieldExtensionComponentProps< // @public export type FieldExtensionOptions< TFieldReturnValue = unknown, - TProps = FieldProps, + TInputProps = unknown, > = { name: string; - component: (props: TProps) => JSX.Element | null; + component: ( + props: FieldExtensionComponentProps, + ) => JSX.Element | null; validation?: CustomFieldValidator; }; -// Warning: (ae-missing-release-tag) "JobStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; - -// Warning: (ae-missing-release-tag) "ListActionsResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type ListActionsResponse = Array<{ id: string; description?: string; @@ -138,9 +113,7 @@ export type ListActionsResponse = Array<{ }; }>; -// Warning: (ae-missing-release-tag) "LogEvent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type LogEvent = { type: 'log' | 'completion'; body: { @@ -153,21 +126,13 @@ export type LogEvent = { taskId: string; }; -// Warning: (ae-missing-release-tag) "OwnedEntityPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const OwnedEntityPicker: ( - props: FieldExtensionComponentProps, -) => JSX.Element; +export const OwnedEntityPickerFieldExtension: FieldExtensionComponent< + string, + OwnedEntityPickerUiOptions +>; -// Warning: (ae-missing-release-tag) "OwnedEntityPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const OwnedEntityPickerFieldExtension: () => null; - -// Warning: (ae-missing-release-tag) "OwnedEntityPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface OwnedEntityPickerUiOptions { // (undocumented) allowedKinds?: string[]; @@ -175,29 +140,19 @@ export interface OwnedEntityPickerUiOptions { defaultKind?: string; } -// Warning: (ae-missing-release-tag) "OwnerPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const OwnerPicker: ( - props: FieldExtensionComponentProps, -) => JSX.Element; +export const OwnerPickerFieldExtension: FieldExtensionComponent< + string, + OwnerPickerUiOptions +>; -// Warning: (ae-missing-release-tag) "OwnerPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const OwnerPickerFieldExtension: () => null; - -// Warning: (ae-missing-release-tag) "OwnerPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface OwnerPickerUiOptions { // (undocumented) allowedKinds?: string[]; } -// Warning: (ae-missing-release-tag) "repoPickerValidation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const repoPickerValidation: ( value: string, validation: FieldValidation, @@ -206,21 +161,13 @@ export const repoPickerValidation: ( }, ) => void; -// Warning: (ae-missing-release-tag) "RepoUrlPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const RepoUrlPicker: ( - props: FieldExtensionComponentProps, -) => JSX.Element; +export const RepoUrlPickerFieldExtension: FieldExtensionComponent< + string, + RepoUrlPickerUiOptions +>; -// Warning: (ae-missing-release-tag) "RepoUrlPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const RepoUrlPickerFieldExtension: () => null; - -// Warning: (ae-missing-release-tag) "RepoUrlPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface RepoUrlPickerUiOptions { // (undocumented) allowedHosts?: string[]; @@ -238,20 +185,12 @@ export interface RepoUrlPickerUiOptions { }; } -// Warning: (ae-missing-release-tag) "RouterProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type RouterProps = { - TemplateCardComponent?: - | ComponentType<{ - template: TemplateEntityV1beta2; - }> - | undefined; - TaskPageComponent?: ComponentType<{}>; components?: { TemplateCardComponent?: | ComponentType<{ - template: TemplateEntityV1beta2; + template: TemplateEntityV1beta3; }> | undefined; TaskPageComponent?: ComponentType<{}>; @@ -313,22 +252,16 @@ export class ScaffolderClient implements ScaffolderApi { streamLogs(options: ScaffolderStreamLogsOptions): Observable; } -// Warning: (ae-missing-release-tag) "ScaffolderFieldExtensions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const ScaffolderFieldExtensions: React_2.ComponentType; -// Warning: (ae-missing-release-tag) "ScaffolderGetIntegrationsListOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface ScaffolderGetIntegrationsListOptions { // (undocumented) allowedHosts: string[]; } -// Warning: (ae-missing-release-tag) "ScaffolderGetIntegrationsListResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface ScaffolderGetIntegrationsListResponse { // (undocumented) integrations: { @@ -338,14 +271,18 @@ export interface ScaffolderGetIntegrationsListResponse { }[]; } -// Warning: (ae-missing-release-tag) "ScaffolderPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) +export type ScaffolderOutputLink = { + title?: string; + icon?: string; + url?: string; + entityRef?: string; +}; + +// @public export const ScaffolderPage: (props: RouterProps) => JSX.Element; -// Warning: (ae-missing-release-tag) "scaffolderPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const scaffolderPlugin: BackstagePlugin< { root: RouteRef; @@ -355,9 +292,7 @@ export const scaffolderPlugin: BackstagePlugin< } >; -// Warning: (ae-missing-release-tag) "ScaffolderScaffoldOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface ScaffolderScaffoldOptions { // (undocumented) secrets?: Record; @@ -367,17 +302,13 @@ export interface ScaffolderScaffoldOptions { values: Record; } -// Warning: (ae-missing-release-tag) "ScaffolderScaffoldResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface ScaffolderScaffoldResponse { // (undocumented) taskId: string; } -// Warning: (ae-missing-release-tag) "ScaffolderStreamLogsOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface ScaffolderStreamLogsOptions { // (undocumented) after?: number; @@ -385,9 +316,7 @@ export interface ScaffolderStreamLogsOptions { taskId: string; } -// Warning: (ae-missing-release-tag) "ScaffolderTask" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type ScaffolderTask = { id: string; spec: TaskSpec; @@ -396,20 +325,14 @@ export type ScaffolderTask = { createdAt: string; }; -// Warning: (ae-missing-release-tag) "ScaffolderTaskOutput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type ScaffolderTaskOutput = { - entityRef?: string; - remoteUrl?: string; links?: ScaffolderOutputLink[]; } & { [key: string]: unknown; }; -// Warning: (ae-missing-release-tag) "ScaffolderTaskStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type ScaffolderTaskStatus = | 'open' | 'processing' @@ -419,8 +342,6 @@ export type ScaffolderTaskStatus = // @public export interface ScaffolderUseTemplateSecrets { - // @deprecated (undocumented) - setSecret: (input: Record) => void; // (undocumented) setSecrets: (input: Record) => void; } @@ -433,33 +354,7 @@ export type TaskPageProps = { loadingText?: string; }; -// Warning: (ae-missing-release-tag) "TemplateList" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export const TemplateList: ({ - TemplateCardComponent, - group, -}: TemplateListProps) => JSX.Element | null; - -// Warning: (ae-missing-release-tag) "TemplateListProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export type TemplateListProps = { - TemplateCardComponent?: - | ComponentType<{ - template: TemplateEntityV1beta2; - }> - | undefined; - group?: { - title?: React_2.ReactNode; - titleComponent?: React_2.ReactNode; - filter: (entity: Entity) => boolean; - }; -}; - -// Warning: (ae-missing-release-tag) "TemplateParameterSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type TemplateParameterSchema = { title: string; steps: Array<{ @@ -468,15 +363,9 @@ export type TemplateParameterSchema = { }>; }; -// Warning: (ae-missing-release-tag) "TemplateTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const TemplateTypePicker: () => JSX.Element | null; // @public export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets; - -// Warnings were encountered during analysis: -// -// src/types.d.ts:32:5 - (ae-forgotten-export) The symbol "ScaffolderOutputLink" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/scaffolder/dev/index.tsx b/plugins/scaffolder/dev/index.tsx index 78e6254e09..03632f434d 100644 --- a/plugins/scaffolder/dev/index.tsx +++ b/plugins/scaffolder/dev/index.tsx @@ -20,16 +20,12 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { catalogApiRef, starredEntitiesApiRef, - DefaultStarredEntitiesApi, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import React from 'react'; import { scaffolderApiRef, ScaffolderClient } from '../src'; import { ScaffolderPage } from '../src/plugin'; -import { - discoveryApiRef, - fetchApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; +import { discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api'; import { CatalogEntityPage } from '@backstage/plugin-catalog'; createDevApp() @@ -44,8 +40,8 @@ createDevApp() }) .registerApi({ api: starredEntitiesApiRef, - deps: { storageApi: storageApiRef }, - factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }), + deps: {}, + factory: () => new MockStarredEntitiesApi(), }) .registerApi({ api: scaffolderApiRef, diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index e4eada7a51..fc206a00d8 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "0.13.0", + "version": "0.15.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,18 +35,18 @@ }, "dependencies": { "@types/json-schema": "^7.0.9", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-catalog-common": "^0.1.4", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/plugin-permission-react": "^0.3.2", - "@backstage/plugin-scaffolder-common": "^0.2.2", + "@backstage/integration": "^0.8.0", + "@backstage/integration-react": "^0.1.25-next.0", + "@backstage/plugin-catalog-common": "^0.2.2-next.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", + "@backstage/plugin-permission-react": "^0.3.3", + "@backstage/plugin-scaffolder-common": "^0.3.0-next.0", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", @@ -73,11 +73,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/plugin-catalog": "^0.10.0-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/scaffolder/src/components/FavouriteTemplate/FavouriteTemplate.tsx b/plugins/scaffolder/src/components/FavouriteTemplate/FavouriteTemplate.tsx deleted file mode 100644 index 6e8fe45b2f..0000000000 --- a/plugins/scaffolder/src/components/FavouriteTemplate/FavouriteTemplate.tsx +++ /dev/null @@ -1,74 +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 { Entity } from '@backstage/catalog-model'; -import { useStarredEntity } from '@backstage/plugin-catalog-react'; -import { IconButton, makeStyles, Tooltip, withStyles } from '@material-ui/core'; -import Star from '@material-ui/icons/Star'; -import StarBorder from '@material-ui/icons/StarBorder'; -import React, { ComponentProps } from 'react'; - -type Props = ComponentProps & { entity: Entity }; - -const YellowStar = withStyles({ - root: { - color: '#f3ba37', - }, -})(Star); - -const WhiteBorderStar = withStyles({ - root: { - color: '#ffffff', - }, -})(StarBorder); - -const useStyles = makeStyles(theme => ({ - starButton: { - position: 'absolute', - top: theme.spacing(0.5), - right: theme.spacing(0.5), - padding: '0.25rem', - }, -})); - -export const favouriteTemplateTooltip = (isStarred: boolean) => - isStarred ? 'Remove from favorites' : 'Add to favorites'; - -export const favouriteTemplateIcon = (isStarred: boolean) => - isStarred ? : ; - -/** - * IconButton for showing if a current entity is starred and adding/removing it from the favourite entities - * @param props - MaterialUI IconButton props extended by required `entity` prop - */ -export const FavouriteTemplate = (props: Props) => { - const classes = useStyles(); - const { toggleStarredEntity, isStarredEntity } = useStarredEntity( - props.entity, - ); - return ( - toggleStarredEntity()} - > - - {favouriteTemplateIcon(isStarredEntity)} - - - ); -}; diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index a456f2bdd3..261556d638 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -17,7 +17,7 @@ import React, { ComponentType } from 'react'; import { Routes, Route, useOutlet } from 'react-router'; import { Entity } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScaffolderPage } from './ScaffolderPage'; import { TemplatePage } from './TemplatePage'; import { TaskPage } from './TaskPage'; @@ -32,17 +32,14 @@ import { } from '../extensions'; import { useElementFilter } from '@backstage/core-plugin-api'; +/** + * The props for the entrypoint `ScaffolderPage` component the plugin. + * @public + */ export type RouterProps = { - /** @deprecated use components.TemplateCardComponent instead */ - TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta2 }> - | undefined; - /** @deprecated use component.TaskPageComponent instead */ - TaskPageComponent?: ComponentType<{}>; - components?: { TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta2 }> + | ComponentType<{ template: TemplateEntityV1beta3 }> | undefined; TaskPageComponent?: ComponentType<{}>; }; @@ -53,26 +50,18 @@ export type RouterProps = { }>; }; +/** + * The main entrypoint `Router` for the `ScaffolderPlugin`. + * + * @public + */ export const Router = (props: RouterProps) => { - const { - TemplateCardComponent: legacyTemplateCardComponent, - TaskPageComponent: legacyTaskPageComponent, - groups, - components = {}, - } = props; - - if (legacyTemplateCardComponent || legacyTaskPageComponent) { - // eslint-disable-next-line no-console - console.warn( - "DEPRECATION: 'TemplateCardComponent' and 'TaskPageComponent' are deprecated when calling the 'ScaffolderPage'. Use 'components' prop to pass these component overrides instead.", - ); - } + const { groups, components = {} } = props; const { TemplateCardComponent, TaskPageComponent } = components; const outlet = useOutlet(); - const TaskPageElement = - TaskPageComponent ?? legacyTaskPageComponent ?? TaskPage; + const TaskPageElement = TaskPageComponent ?? TaskPage; const customFieldExtensions = useElementFilter(outlet, elements => elements @@ -101,9 +90,7 @@ export const Router = (props: RouterProps) => { element={ } /> diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index da9f1f74b8..0b3fccc07c 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -24,7 +24,7 @@ import { SupportButton, } from '@backstage/core-components'; import { Entity } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { useRouteRef } from '@backstage/core-plugin-api'; import { EntityKindPicker, @@ -52,7 +52,7 @@ const useStyles = makeStyles(theme => ({ export type ScaffolderPageProps = { TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta2 }> + | ComponentType<{ template: TemplateEntityV1beta3 }> | undefined; groups?: Array<{ title?: React.ReactNode; diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index e0445ed641..f73e32189c 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -217,12 +217,8 @@ export const TaskStatusStepper = memo( }, ); -const hasLinks = ({ - entityRef, - remoteUrl, - links = [], -}: ScaffolderTaskOutput): boolean => - !!(entityRef || remoteUrl || links.length > 0); +const hasLinks = ({ links = [] }: ScaffolderTaskOutput): boolean => + links.length > 0; /** * TaskPageProps for constructing a TaskPage @@ -302,10 +298,7 @@ export const TaskPage = ({ loadingText }: TaskPageProps) => { return; } - const formData = - taskStream.task!.spec.apiVersion === 'backstage.io/v1beta2' - ? taskStream.task!.spec.values - : taskStream.task!.spec.parameters; + const formData = taskStream.task!.spec.parameters; const { name } = parseEntityRef( taskStream.task!.spec.templateInfo?.entityRef, diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.test.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.test.tsx index 8316b67ff5..2f5d015f8d 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.test.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.test.tsx @@ -26,43 +26,6 @@ describe('TaskPageLinks', () => { jest.resetAllMocks(); }); - it('renders the entityRef link', async () => { - const output = { entityRef: 'Component:default/my-app' }; - const { findByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, - }, - }, - ); - - const element = await findByText('Open in catalog'); - - expect(element).toBeInTheDocument(); - expect(element).toHaveAttribute( - 'href', - '/catalog/default/Component/my-app', - ); - }); - - it('renders the remoteUrl link', async () => { - const output = { remoteUrl: 'https://remote.url' }; - const { findByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, - }, - }, - ); - - const element = await findByText('Repo'); - - expect(element).toBeInTheDocument(); - expect(element).toHaveAttribute('href', 'https://remote.url'); - }); - it('renders further links', async () => { const output = { links: [ diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx index 5451b4e725..cb77689b6e 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx @@ -28,29 +28,13 @@ type TaskPageLinksProps = { }; export const TaskPageLinks = ({ output }: TaskPageLinksProps) => { - const { entityRef: entityRefOutput, remoteUrl } = output; - let { links = [] } = output; + const { links = [] } = output; const app = useApp(); const entityRoute = useRouteRef(entityRouteRef); const iconResolver = (key?: string): IconComponent => key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon; - if (remoteUrl) { - links = [{ url: remoteUrl, title: 'Repo' }, ...links]; - } - - if (entityRefOutput) { - links = [ - { - entityRef: entityRefOutput, - title: 'Open in catalog', - icon: 'catalog', - }, - ...links, - ]; - } - return ( {links diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index f4710427c2..54f8188e3b 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -14,13 +14,14 @@ * limitations under the License. */ import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScmIntegrationIcon, scmIntegrationsApiRef, } from '@backstage/integration-react'; import { EntityRefLinks, + FavoriteEntity, getEntityRelations, getEntitySourceLocation, } from '@backstage/plugin-catalog-react'; @@ -42,9 +43,12 @@ import { import WarningIcon from '@material-ui/icons/Warning'; import React from 'react'; import { selectedTemplateRouteRef } from '../../routes'; -import { FavouriteTemplate } from '../FavouriteTemplate/FavouriteTemplate'; -import { Button, ItemCardHeader } from '@backstage/core-components'; +import { + Button, + ItemCardHeader, + MarkdownContent, +} from '@backstage/core-components'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; const useStyles = makeStyles(theme => ({ @@ -74,6 +78,12 @@ const useStyles = makeStyles(theme => ({ leftButton: { marginRight: 'auto', }, + starButton: { + position: 'absolute', + top: theme.spacing(0.5), + right: theme.spacing(0.5), + padding: '0.25rem', + }, })); const useDeprecationStyles = makeStyles(theme => ({ @@ -89,7 +99,7 @@ const useDeprecationStyles = makeStyles(theme => ({ })); export type TemplateCardProps = { - template: TemplateEntityV1beta2; + template: TemplateEntityV1beta3; deprecated?: boolean; }; @@ -102,7 +112,7 @@ type TemplateProps = { }; const getTemplateCardProps = ( - template: TemplateEntityV1beta2, + template: TemplateEntityV1beta3, ): TemplateProps & { key: string } => { return { key: template.metadata.uid!, @@ -159,7 +169,7 @@ export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => { return ( - + {deprecated && } { Description - {templateProps.description} + diff --git a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx index 3d9da542de..e3cf6eac05 100644 --- a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx +++ b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx @@ -16,7 +16,7 @@ import React, { ComponentType } from 'react'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { Content, ContentHeader, @@ -34,7 +34,7 @@ import { TemplateCard } from '../TemplateCard'; */ export type TemplateListProps = { TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta2 }> + | ComponentType<{ template: TemplateEntityV1beta3 }> | undefined; group?: { title?: React.ReactNode; @@ -106,7 +106,7 @@ export const TemplateList = ({ maybeFilteredEntities.map((template: Entity) => ( ))} diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index e69c4bab5c..5310a4c79f 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -109,7 +109,7 @@ export const createValidator = ( export const TemplatePage = ({ customFieldExtensions = [], }: { - customFieldExtensions?: FieldExtensionOptions[]; + customFieldExtensions?: FieldExtensionOptions[]; }) => { const apiHolder = useApiHolder(); const secretsContext = useContext(SecretsContext); diff --git a/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.tsx b/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.tsx index 0f18b34bb9..e7c2c965f2 100644 --- a/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.tsx +++ b/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.tsx @@ -34,6 +34,11 @@ import { alertApiRef, useApi } from '@backstage/core-plugin-api'; const icon = ; const checkedIcon = ; +/** + * The component to select the `type` of `Template` that you will see in the table. + * + * @public + */ export const TemplateTypePicker = () => { const alertApi = useApi(alertApiRef); const { error, loading, availableTypes, selectedTypes, setSelectedTypes } = diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts index 0ba88d1277..7076221480 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { EntityNamePicker } from './EntityNamePicker'; export { entityNamePickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index 31c44bfe97..c899b56683 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -16,7 +16,7 @@ import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef, - formatEntityRefTitle, + humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { TextField } from '@material-ui/core'; import FormControl from '@material-ui/core/FormControl'; @@ -25,6 +25,12 @@ import React, { useCallback, useEffect } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { FieldExtensionComponentProps } from '../../../extensions'; +/** + * The input props that can be specified under `ui:options` for the + * `EntityPicker` field extension. + * + * @public + */ export interface EntityPickerUiOptions { allowedKinds?: string[]; defaultKind?: string; @@ -32,7 +38,10 @@ export interface EntityPickerUiOptions { } /** - * Entity Picker + * The underlying component that is rendered in the form for the `EntityPicker` + * field extension. + * + * @public */ export const EntityPicker = ( props: FieldExtensionComponentProps, @@ -58,7 +67,7 @@ export const EntityPicker = ( ); const entityRefs = entities?.items.map(e => - formatEntityRefTitle(e, { defaultKind }), + humanizeEntityRef(e, { defaultKind }), ); const onSelect = useCallback( diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts index eb7511bbef..891b5bef16 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { EntityPicker } from './EntityPicker'; export type { EntityPickerUiOptions } from './EntityPicker'; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx index 41369a58eb..5cf98cb8aa 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx @@ -24,11 +24,21 @@ import { FormControl, TextField } from '@material-ui/core'; import { Autocomplete } from '@material-ui/lab'; import { FieldExtensionComponentProps } from '../../../extensions'; +/** + * The input props that can be specified under `ui:options` for the + * `EntityTagsPicker` field extension. + * + * @public + */ export interface EntityTagsPickerUiOptions { kinds?: string[]; } + /** - * EntityTagsPicker + * The underlying component that is rendered in the form for the `EntityTagsPicker` + * field extension. + * + * @public */ export const EntityTagsPicker = ( props: FieldExtensionComponentProps, diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts index 3ba36a2409..9ff6e553a6 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { EntityTagsPicker } from './EntityTagsPicker'; export type { EntityTagsPickerUiOptions } from './EntityTagsPicker'; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx index 80f63efaf9..8e3590e8bf 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx @@ -13,24 +13,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { GetEntitiesResponse } from '@backstage/catalog-client'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { identityApiRef, useApi } from '@backstage/core-plugin-api'; import { - formatEntityRefTitle, - useOwnedEntities, + catalogApiRef, + humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { TextField } from '@material-ui/core'; import FormControl from '@material-ui/core/FormControl'; import Autocomplete from '@material-ui/lab/Autocomplete'; -import React from 'react'; +import React, { useMemo } from 'react'; +import useAsync from 'react-use/lib/useAsync'; import { FieldExtensionComponentProps } from '../../../extensions'; +/** + * The input props that can be specified under `ui:options` for the + * `OwnedEntityPicker` field extension. + * + * @public + */ export interface OwnedEntityPickerUiOptions { allowedKinds?: string[]; defaultKind?: string; } /** - * Owned Entity Picker + * The underlying component that is rendered in the form for the `OwnedEntityPicker` + * field extension. + * + * @public */ export const OwnedEntityPicker = ( props: FieldExtensionComponentProps, @@ -50,7 +63,7 @@ export const OwnedEntityPicker = ( const { ownedEntities, loading } = useOwnedEntities(allowedKinds); const entityRefs = ownedEntities?.items - .map(e => formatEntityRefTitle(e, { defaultKind })) + .map(e => humanizeEntityRef(e, { defaultKind })) .filter(n => n); const onSelect = (_: any, value: string | null) => { @@ -86,3 +99,45 @@ export const OwnedEntityPicker = ( ); }; + +/** + * Takes the relevant parts of the Backstage identity, and translates them into + * a list of entities which are owned by the user. Takes an optional parameter + * to filter the entities based on allowedKinds + * + * + * @param allowedKinds - Array of allowed kinds to filter the entities + */ +function useOwnedEntities(allowedKinds?: string[]): { + loading: boolean; + ownedEntities: GetEntitiesResponse | undefined; +} { + const identityApi = useApi(identityApiRef); + const catalogApi = useApi(catalogApiRef); + + const { loading, value: refs } = useAsync(async () => { + const identity = await identityApi.getBackstageIdentity(); + const identityRefs = identity.ownershipEntityRefs; + const catalogs = await catalogApi.getEntities( + allowedKinds + ? { + filter: { + kind: allowedKinds, + [`relations.${RELATION_OWNED_BY}`]: identityRefs || [], + }, + } + : { + filter: { + [`relations.${RELATION_OWNED_BY}`]: identityRefs || [], + }, + }, + ); + return catalogs; + }, []); + + const ownedEntities = useMemo(() => { + return refs; + }, [refs]); + + return useMemo(() => ({ loading, ownedEntities }), [loading, ownedEntities]); +} diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts index 125f6fdef0..2988ba8cdc 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { OwnedEntityPicker } from './OwnedEntityPicker'; export type { OwnedEntityPickerUiOptions } from './OwnedEntityPicker'; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx index 9012ed149e..a0c22b7fef 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx @@ -14,15 +14,24 @@ * limitations under the License. */ import React from 'react'; -import { EntityPicker } from '../EntityPicker'; +import { EntityPicker } from '../EntityPicker/EntityPicker'; import { FieldExtensionComponentProps } from '../../../extensions'; +/** + * The input props that can be specified under `ui:options` for the + * `OwnerPicker` field extension. + * + * @public + */ export interface OwnerPickerUiOptions { allowedKinds?: string[]; } /** - * Owner Picker + * The underlying component that is rendered in the form for the `OwnerPicker` + * field extension. + * + * @public */ export const OwnerPicker = ( props: FieldExtensionComponentProps, diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts index b8cb97eb97..aa26024b5b 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { OwnerPicker } from './OwnerPicker'; export type { OwnerPickerUiOptions } from './OwnerPicker'; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 17fd5ef665..fbb7bcd821 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -30,6 +30,12 @@ import { RepoUrlPickerState } from './types'; import useDebounce from 'react-use/lib/useDebounce'; import { useTemplateSecrets } from '../../secrets'; +/** + * The input props that can be specified under `ui:options` for the + * `RepoUrlPicker` field extension. + * + * @public + */ export interface RepoUrlPickerUiOptions { allowedHosts?: string[]; allowedOwners?: string[]; @@ -45,7 +51,10 @@ export interface RepoUrlPickerUiOptions { } /** - * Repo Url Picker + * The underlying component that is rendered in the form for the `RepoUrlPicker` + * field extension. + * + * @public */ export const RepoUrlPicker = ( props: FieldExtensionComponentProps, diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts index 2fdbf0aac9..c5f596a786 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts @@ -13,6 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { RepoUrlPicker } from './RepoUrlPicker'; export type { RepoUrlPickerUiOptions } from './RepoUrlPicker'; export { repoPickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts index 24a1c2f5ce..ab5435f5d8 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts @@ -18,6 +18,13 @@ import { FieldValidation } from '@rjsf/core'; import { ApiHolder } from '@backstage/core-plugin-api'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; +/** + * The validation function for the `repoUrl` that is returned from the + * field extension. Ensures that you have all the required fields filled for + * the different providers that exist. + * + * @public + */ export const repoPickerValidation = ( value: string, validation: FieldValidation, diff --git a/plugins/scaffolder/src/components/index.ts b/plugins/scaffolder/src/components/index.ts index 030c485fd7..d0442cb906 100644 --- a/plugins/scaffolder/src/components/index.ts +++ b/plugins/scaffolder/src/components/index.ts @@ -15,9 +15,6 @@ */ export * from './fields'; export type { RepoUrlPickerUiOptions } from './fields'; -export { FavouriteTemplate } from './FavouriteTemplate'; -export { TemplateList } from './TemplateList'; -export type { TemplateListProps } from './TemplateList'; export { TemplateTypePicker } from './TemplateTypePicker'; export * from './secrets'; export { TaskPage } from './TaskPage'; diff --git a/plugins/scaffolder/src/components/secrets/SecretsContext.tsx b/plugins/scaffolder/src/components/secrets/SecretsContext.tsx index f0b62f20a1..b425a72233 100644 --- a/plugins/scaffolder/src/components/secrets/SecretsContext.tsx +++ b/plugins/scaffolder/src/components/secrets/SecretsContext.tsx @@ -53,8 +53,6 @@ export const SecretsContextProvider = ({ children }: PropsWithChildren<{}>) => { * @public */ export interface ScaffolderUseTemplateSecrets { - /** @deprecated use setSecrets instead */ - setSecret: (input: Record) => void; setSecrets: (input: Record) => void; } @@ -79,5 +77,5 @@ export const useTemplateSecrets = (): ScaffolderUseTemplateSecrets => { [updateSecrets], ); - return { setSecret: setSecrets, setSecrets }; + return { setSecrets }; }; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 10c0746e41..8081dcb0ca 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -13,46 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityPicker } from '../components/fields/EntityPicker'; -import { - EntityNamePicker, - entityNamePickerValidation, -} from '../components/fields/EntityNamePicker'; -import { EntityTagsPicker } from '../components/fields/EntityTagsPicker'; -import { OwnerPicker } from '../components/fields/OwnerPicker'; -import { - repoPickerValidation, - RepoUrlPicker, -} from '../components/fields/RepoUrlPicker'; -import { FieldExtensionOptions } from './types'; -import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker'; +import { EntityPicker } from '../components/fields/EntityPicker/EntityPicker'; +import { EntityNamePicker } from '../components/fields/EntityNamePicker/EntityNamePicker'; +import { entityNamePickerValidation } from '../components/fields/EntityNamePicker/validation'; +import { EntityTagsPicker } from '../components/fields/EntityTagsPicker/EntityTagsPicker'; +import { OwnerPicker } from '../components/fields/OwnerPicker/OwnerPicker'; +import { RepoUrlPicker } from '../components/fields/RepoUrlPicker/RepoUrlPicker'; +import { repoPickerValidation } from '../components/fields/RepoUrlPicker/validation'; +import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; -export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS: FieldExtensionOptions[] = - [ - { - component: EntityPicker, - name: 'EntityPicker', - }, - { - component: EntityNamePicker, - name: 'EntityNamePicker', - validation: entityNamePickerValidation, - }, - { - component: EntityTagsPicker, - name: 'EntityTagsPicker', - }, - { - component: RepoUrlPicker, - name: 'RepoUrlPicker', - validation: repoPickerValidation, - }, - { - component: OwnerPicker, - name: 'OwnerPicker', - }, - { - component: OwnedEntityPicker, - name: 'OwnedEntityPicker', - }, - ]; +export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ + { + component: EntityPicker, + name: 'EntityPicker', + }, + { + component: EntityNamePicker, + name: 'EntityNamePicker', + validation: entityNamePickerValidation, + }, + { + component: EntityTagsPicker, + name: 'EntityTagsPicker', + }, + { + component: RepoUrlPicker, + name: 'RepoUrlPicker', + validation: repoPickerValidation, + }, + { + component: OwnerPicker, + name: 'OwnerPicker', + }, + { + component: OwnedEntityPicker, + name: 'OwnedEntityPicker', + }, +]; diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index 53db4cd927..60b9022053 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -25,14 +25,24 @@ import { Extension, attachComponentData } from '@backstage/core-plugin-api'; export const FIELD_EXTENSION_WRAPPER_KEY = 'scaffolder.extensions.wrapper.v1'; export const FIELD_EXTENSION_KEY = 'scaffolder.extensions.field.v1'; +/** + * A type used to wrap up the FieldExtension to embed the ReturnValue and the InputProps + * + * @public + */ +export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; + +/** + * Method for creating field extensions that can be used in the scaffolder + * frontend form. + * @public + */ export function createScaffolderFieldExtension< TReturnValue = unknown, TInputProps = unknown, >( options: FieldExtensionOptions, - // TODO: need know how to embed these types nicely so the api report looks nice. - // then we can remove the export of the components -): Extension<() => null> { +): Extension> { return { expose() { const FieldExtensionDataHolder: any = () => null; @@ -48,6 +58,11 @@ export function createScaffolderFieldExtension< }; } +/** + * The Wrapping component for defining fields extensions inside + * + * @public + */ export const ScaffolderFieldExtensions: React.ComponentType = (): JSX.Element | null => null; diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder/src/extensions/types.ts index 574ce7ea64..be8dc14b0a 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder/src/extensions/types.ts @@ -35,10 +35,12 @@ export type CustomFieldValidator = ( */ export type FieldExtensionOptions< TFieldReturnValue = unknown, - TProps = FieldProps, + TInputProps = unknown, > = { name: string; - component: (props: TProps) => JSX.Element | null; + component: ( + props: FieldExtensionComponentProps, + ) => JSX.Element | null; validation?: CustomFieldValidator; }; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 71cd1d8f84..bdca2a0952 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -22,12 +22,12 @@ export { scaffolderApiRef, ScaffolderClient } from './api'; export type { - JobStatus, ListActionsResponse, LogEvent, ScaffolderApi, ScaffolderGetIntegrationsListOptions, ScaffolderGetIntegrationsListResponse, + ScaffolderOutputLink, ScaffolderScaffoldOptions, ScaffolderScaffoldResponse, ScaffolderStreamLogsOptions, @@ -44,6 +44,7 @@ export type { CustomFieldValidator, FieldExtensionOptions, FieldExtensionComponentProps, + FieldExtensionComponent, } from './extensions'; export { EntityPickerFieldExtension, diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index ccf8d2ee34..ffa0efaaa0 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -16,16 +16,12 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { scaffolderApiRef, ScaffolderClient } from './api'; -import { EntityPicker } from './components/fields/EntityPicker'; -import { - entityNamePickerValidation, - EntityNamePicker, -} from './components/fields/EntityNamePicker'; -import { OwnerPicker } from './components/fields/OwnerPicker'; -import { - repoPickerValidation, - RepoUrlPicker, -} from './components/fields/RepoUrlPicker'; +import { EntityPicker } from './components/fields/EntityPicker/EntityPicker'; +import { entityNamePickerValidation } from './components/fields/EntityNamePicker'; +import { EntityNamePicker } from './components/fields/EntityNamePicker/EntityNamePicker'; +import { OwnerPicker } from './components/fields/OwnerPicker/OwnerPicker'; +import { repoPickerValidation } from './components/fields/RepoUrlPicker'; +import { RepoUrlPicker } from './components/fields/RepoUrlPicker/RepoUrlPicker'; import { createScaffolderFieldExtension } from './extensions'; import { registerComponentRouteRef, rootRouteRef } from './routes'; import { @@ -35,9 +31,13 @@ import { discoveryApiRef, fetchApiRef, } from '@backstage/core-plugin-api'; -import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker'; -import { EntityTagsPicker } from './components/fields/EntityTagsPicker'; +import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker/OwnedEntityPicker'; +import { EntityTagsPicker } from './components/fields/EntityTagsPicker/EntityTagsPicker'; +/** + * The main plugin export for the scaffolder. + * @public + */ export const scaffolderPlugin = createPlugin({ id: 'scaffolder', apis: [ @@ -64,6 +64,11 @@ export const scaffolderPlugin = createPlugin({ }, }); +/** + * A field extension for selecting an Entity that exists in the Catalog. + * + * @public + */ export const EntityPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: EntityPicker, @@ -71,6 +76,11 @@ export const EntityPickerFieldExtension = scaffolderPlugin.provide( }), ); +/** + * The field extension for selecting a name for a new Entity in the Catalog. + * + * @public + */ export const EntityNamePickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: EntityNamePicker, @@ -79,6 +89,12 @@ export const EntityNamePickerFieldExtension = scaffolderPlugin.provide( }), ); +/** + * The field extension which provides the ability to select a RepositoryUrl. + * Currently this is an encoded URL that looks something like the following `github.com?repo=myRepoName&owner=backstage`. + * + * @public + */ export const RepoUrlPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: RepoUrlPicker, @@ -87,6 +103,11 @@ export const RepoUrlPickerFieldExtension = scaffolderPlugin.provide( }), ); +/** + * A field extension for picking users and groups out of the Catalog. + * + * @public + */ export const OwnerPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: OwnerPicker, @@ -94,6 +115,11 @@ export const OwnerPickerFieldExtension = scaffolderPlugin.provide( }), ); +/** + * The Router and main entrypoint to the Scaffolder plugin. + * + * @public + */ export const ScaffolderPage = scaffolderPlugin.provide( createRoutableExtension({ name: 'ScaffolderPage', @@ -102,6 +128,11 @@ export const ScaffolderPage = scaffolderPlugin.provide( }), ); +/** + * A field extension to show all the Entities that are owned by the current logged-in User for use in templates. + * + * @public + */ export const OwnedEntityPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: OwnedEntityPicker, diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index 0ffbd601a8..a542a8a473 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -17,6 +17,11 @@ import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { JsonObject, JsonValue, Observable } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; +/** + * The status of each task in a Scaffolder Job + * + * @public + */ export type ScaffolderTaskStatus = | 'open' | 'processing' @@ -24,8 +29,11 @@ export type ScaffolderTaskStatus = | 'completed' | 'skipped'; -export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; - +/** + * The shape of each task returned from the `scaffolder-backend` + * + * @public + */ export type ScaffolderTask = { id: string; spec: TaskSpec; @@ -34,6 +42,11 @@ export type ScaffolderTask = { createdAt: string; }; +/** + * The response shape for the `listActions` call to the `scaffolder-backend` + * + * @public + */ export type ListActionsResponse = Array<{ id: string; description?: string; @@ -43,23 +56,27 @@ export type ListActionsResponse = Array<{ }; }>; -type ScaffolderOutputLink = { +/** @public */ +export type ScaffolderOutputLink = { title?: string; icon?: string; url?: string; entityRef?: string; }; +/** @public */ export type ScaffolderTaskOutput = { - /** @deprecated use the `links` property to link out to relevant resources */ - entityRef?: string; - /** @deprecated use the `links` property to link out to relevant resources */ - remoteUrl?: string; links?: ScaffolderOutputLink[]; } & { [key: string]: unknown; }; +/** + * The shape of each entry of parameters which gets rendered + * as a seperate step in the wizard input + * + * @public + */ export type TemplateParameterSchema = { title: string; steps: Array<{ @@ -68,6 +85,11 @@ export type TemplateParameterSchema = { }>; }; +/** + * The shape of a `LogEvent` message from the `scaffolder-backend` + * + * @public + */ export type LogEvent = { type: 'log' | 'completion'; body: { @@ -80,24 +102,49 @@ export type LogEvent = { taskId: string; }; +/** + * The input options to the `scaffold` method of the `ScaffolderClient`. + * + * @public + */ export interface ScaffolderScaffoldOptions { templateRef: string; values: Record; secrets?: Record; } +/** + * The response shape of the `scaffold` method of the `ScaffolderClient`. + * + * @public + */ export interface ScaffolderScaffoldResponse { taskId: string; } +/** + * The arguments for `getIntegrationsList`. + * + * @public + */ export interface ScaffolderGetIntegrationsListOptions { allowedHosts: string[]; } +/** + * The response shape for `getIntegrationsList`. + * + * @public + */ export interface ScaffolderGetIntegrationsListResponse { integrations: { type: string; title: string; host: string }[]; } +/** + * The input options to the `streamLogs` method of the `ScaffolderClient`. + * + * @public + */ export interface ScaffolderStreamLogsOptions { taskId: string; after?: number; diff --git a/plugins/search-backend-module-elasticsearch/.eslintrc.js b/plugins/search-backend-module-elasticsearch/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/search-backend-module-elasticsearch/.eslintrc.js +++ b/plugins/search-backend-module-elasticsearch/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index b68824932e..e207ccd73e 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 0.1.1-next.0 + +### Patch Changes + +- 3e54f6c436: Use `@backstage/plugin-search-common` package instead of `@backstage/search-common`. +- Updated dependencies + - @backstage/plugin-search-common@0.3.1-next.0 + - @backstage/plugin-search-backend-node@0.5.1-next.0 + +## 0.1.0 + +### Minor Changes + +- 022507c860: **BREAKING** + + The `ElasticSearchSearchEngine` implements the new stream-based indexing + process expected by the latest `@backstage/search-backend-node`. + + When updating to this version, you must also update to the latest version of + `@backstage/search-backend-node`. Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for further details. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@0.5.0 + - @backstage/search-common@0.3.0 + ## 0.0.10 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index 6dc0256586..265fdda686 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -5,13 +5,15 @@ ```ts /// +import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; +import { Client } from '@elastic/elasticsearch'; import { Config } from '@backstage/config'; import type { ConnectionOptions } from 'tls'; -import { IndexableDocument } from '@backstage/search-common'; -import { Logger as Logger_2 } from 'winston'; -import { SearchEngine } from '@backstage/search-common'; -import { SearchQuery } from '@backstage/search-common'; -import { SearchResultSet } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { Logger } from 'winston'; +import { SearchEngine } from '@backstage/plugin-search-common'; +import { SearchQuery } from '@backstage/plugin-search-common'; +import { SearchResultSet } from '@backstage/plugin-search-common'; // Warning: (ae-missing-release-tag) "ElasticSearchClientOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-search-backend-module-elasticsearch" does not have an export "ElasticSearchEngine" @@ -102,7 +104,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { elasticSearchClientOptions: ElasticSearchClientOptions, aliasPostfix: string, indexPrefix: string, - logger: Logger_2, + logger: Logger, ); // Warning: (ae-forgotten-export) The symbol "ElasticSearchOptions" needs to be exported by the entry point index.d.ts // @@ -114,7 +116,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { indexPrefix, }: ElasticSearchOptions): Promise; // (undocumented) - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; newClient(create: (options: ElasticSearchClientOptions) => T): T; // (undocumented) query(query: SearchQuery): Promise; @@ -127,4 +129,31 @@ export class ElasticSearchSearchEngine implements SearchEngine { // (undocumented) protected translator(query: SearchQuery): ConcreteElasticSearchQuery; } + +// Warning: (ae-missing-release-tag) "ElasticSearchSearchEngineIndexer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { + constructor(options: ElasticSearchSearchEngineIndexerOptions); + // (undocumented) + finalize(): Promise; + // (undocumented) + index(documents: IndexableDocument[]): Promise; + // (undocumented) + readonly indexName: string; + // (undocumented) + initialize(): Promise; +} + +// Warning: (ae-missing-release-tag) "ElasticSearchSearchEngineIndexerOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ElasticSearchSearchEngineIndexerOptions = { + type: string; + indexPrefix: string; + indexSeparator: string; + alias: string; + logger: Logger; + elasticSearchClient: Client; +}; ``` diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 2a2dbd6f85..b3205f56c3 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", "description": "A module for the search backend that implements search using ElasticSearch", - "version": "0.0.10", + "version": "0.1.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,7 +24,8 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/search-common": "^0.2.4", + "@backstage/plugin-search-backend-node": "^0.5.1-next.0", + "@backstage/plugin-search-common": "^0.3.1-next.0", "@elastic/elasticsearch": "7.13.0", "@acuris/aws-es-connection": "^2.2.0", "aws-sdk": "^2.948.0", @@ -33,8 +34,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/cli": "^0.14.1", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/cli": "^0.15.2-next.0", "@elastic/elasticsearch-mock": "^1.0.0" }, "files": [ diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts index 1566a35b57..467e1b4e77 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts @@ -15,7 +15,8 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { Client } from '@elastic/elasticsearch'; +import { ConfigReader } from '@backstage/config'; +import { Client, errors } from '@elastic/elasticsearch'; import Mock from '@elastic/elasticsearch-mock'; import { ConcreteElasticSearchQuery, @@ -23,7 +24,7 @@ import { ElasticSearchSearchEngine, encodePageCursor, } from './ElasticSearchSearchEngine'; -import { ConfigReader } from '@backstage/config'; +import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer'; class ElasticSearchSearchEngineForTranslatorTests extends ElasticSearchSearchEngine { getTranslator() { @@ -37,6 +38,16 @@ const options = { Connection: mock.getConnection(), }; +const indexerMock = { + on: jest.fn(), + indexName: 'expected-index-name', +}; +jest.mock('./ElasticSearchSearchEngineIndexer', () => ({ + ElasticSearchSearchEngineIndexer: jest + .fn() + .mockImplementation(() => indexerMock), +})); + describe('ElasticSearchSearchEngine', () => { let testSearchEngine: ElasticSearchSearchEngine; let inspectableSearchEngine: ElasticSearchSearchEngineForTranslatorTests; @@ -542,23 +553,67 @@ describe('ElasticSearchSearchEngine', () => { }); }); - describe('index', () => { - it('should index document', async () => { - const indexSpy = jest.spyOn(testSearchEngine, 'index'); - const mockDocuments = [ - { - title: 'testTerm', - text: 'testText', - location: 'test/location', - }, - ]; + describe('indexer', () => { + it('should get indexer', async () => { + const indexer = await testSearchEngine.getIndexer('test-index'); - // call index func and ensure the index func was invoked. - await testSearchEngine.index('test-index', mockDocuments); - expect(indexSpy).toHaveBeenCalled(); - expect(indexSpy).toHaveBeenCalledWith('test-index', [ - { title: 'testTerm', text: 'testText', location: 'test/location' }, - ]); + expect(indexer).toStrictEqual(indexerMock); + expect(ElasticSearchSearchEngineIndexer).toHaveBeenCalledWith( + expect.objectContaining({ + alias: 'test-index__search', + type: 'test-index', + indexPrefix: '', + indexSeparator: '-index__', + elasticSearchClient: client, + }), + ); + expect(indexerMock.on).toHaveBeenCalledWith( + 'error', + expect.any(Function), + ); + }); + + describe('onError', () => { + let errorHandler: Function; + const error = new Error('some error'); + + beforeEach(async () => { + mock.clearAll(); + await testSearchEngine.getIndexer('test-index'); + errorHandler = indexerMock.on.mock.calls[0][1]; + }); + + it('should check for and delete expected index', async () => { + const existsSpy = jest.fn().mockReturnValue('truthy value'); + const deleteSpy = jest.fn().mockReturnValue({}); + mock.add({ method: 'HEAD', path: '/expected-index-name' }, existsSpy); + mock.add({ method: 'DELETE', path: '/expected-index-name' }, deleteSpy); + + await errorHandler(error); + + // Check and delete HTTP requests were made. + expect(existsSpy).toHaveBeenCalled(); + expect(deleteSpy).toHaveBeenCalled(); + }); + + it('should not delete index if none exists', async () => { + // Exists call returns 404 on no index. + const existsSpy = jest.fn().mockReturnValue( + new errors.ResponseError({ + statusCode: 404, + body: { status: 404 }, + } as unknown as any), + ); + const deleteSpy = jest.fn().mockReturnValue({}); + mock.add({ method: 'HEAD', path: '/expected-index-name' }, existsSpy); + mock.add({ method: 'DELETE', path: '/expected-index-name' }, deleteSpy); + + await errorHandler(error); + + // Check request was made, but no delete request was made. + expect(existsSpy).toHaveBeenCalled(); + expect(deleteSpy).not.toHaveBeenCalled(); + }); }); }); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 48caf0d4f1..f2ed5ed2b0 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -24,13 +24,13 @@ import { SearchEngine, SearchQuery, SearchResultSet, -} from '@backstage/search-common'; +} from '@backstage/plugin-search-common'; import { Client } from '@elastic/elasticsearch'; import esb from 'elastic-builder'; import { isEmpty, isNaN as nan, isNumber } from 'lodash'; import { Logger } from 'winston'; - import type { ElasticSearchClientOptions } from './ElasticSearchClientOptions'; +import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer'; export type { ElasticSearchClientOptions }; @@ -58,12 +58,6 @@ type ElasticSearchResult = { _source: IndexableDocument; }; -function duration(startTimestamp: [number, number]): string { - const delta = process.hrtime(startTimestamp); - const seconds = delta[0] + delta[1] / 1e9; - return `${seconds.toFixed(1)}s`; -} - function isBlank(str: string) { return (isEmpty(str) && !isNumber(str)) || nan(str); } @@ -165,67 +159,37 @@ export class ElasticSearchSearchEngine implements SearchEngine { this.translator = translator; } - async index(type: string, documents: IndexableDocument[]): Promise { - this.logger.info( - `Started indexing ${documents.length} documents for index ${type}`, - ); - const startTimestamp = process.hrtime(); + async getIndexer(type: string) { const alias = this.constructSearchAlias(type); - const index = this.constructIndexName(type, `${Date.now()}`); - try { - const aliases = await this.elasticSearchClient.cat.aliases({ - format: 'json', - name: alias, - }); - const removableIndices = aliases.body.map( - (r: Record) => r.index, - ); + const indexer = new ElasticSearchSearchEngineIndexer({ + type, + indexPrefix: this.indexPrefix, + indexSeparator: this.indexSeparator, + alias, + elasticSearchClient: this.elasticSearchClient, + logger: this.logger, + }); - await this.elasticSearchClient.indices.create({ - index, - }); - const result = await this.elasticSearchClient.helpers.bulk({ - datasource: documents, - onDocument() { - return { - index: { _index: index }, - }; - }, - refreshOnCompletion: index, - }); - - this.logger.info( - `Indexing completed for index ${type} in ${duration(startTimestamp)}`, - result, - ); - await this.elasticSearchClient.indices.updateAliases({ - body: { - actions: [ - { remove: { index: this.constructIndexName(type, '*'), alias } }, - { add: { index, alias } }, - ], - }, - }); - - this.logger.info('Removing stale search indices', removableIndices); - if (removableIndices.length) { - await this.elasticSearchClient.indices.delete({ - index: removableIndices, - }); - } - } catch (e) { + // Attempt cleanup upon failure. + indexer.on('error', async e => { this.logger.error(`Failed to index documents for type ${type}`, e); - const response = await this.elasticSearchClient.indices.exists({ - index, - }); - const indexCreated = response.body; - if (indexCreated) { - this.logger.info(`Removing created index ${index}`); - await this.elasticSearchClient.indices.delete({ - index, + try { + const response = await this.elasticSearchClient.indices.exists({ + index: indexer.indexName, }); + const indexCreated = response.body; + if (indexCreated) { + this.logger.info(`Removing created index ${indexer.indexName}`); + await this.elasticSearchClient.indices.delete({ + index: indexer.indexName, + }); + } + } catch (error) { + this.logger.error(`Unable to clean up elastic index: ${error}`); } - } + }); + + return indexer; } async query(query: SearchQuery): Promise { @@ -268,10 +232,6 @@ export class ElasticSearchSearchEngine implements SearchEngine { private readonly indexSeparator = '-index__'; - private constructIndexName(type: string, postFix: string) { - return `${this.indexPrefix}${type}${this.indexSeparator}${postFix}`; - } - private getTypeFromIndex(index: string) { return index .substring(this.indexPrefix.length) diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts new file mode 100644 index 0000000000..0867887d9a --- /dev/null +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts @@ -0,0 +1,211 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { TestPipeline } from '@backstage/plugin-search-backend-node'; +import { Client } from '@elastic/elasticsearch'; +import Mock from '@elastic/elasticsearch-mock'; +import { range } from 'lodash'; +import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer'; + +const mock = new Mock(); +const client = new Client({ + node: 'http://localhost:9200', + Connection: mock.getConnection(), +}); + +describe('ElasticSearchSearchEngineIndexer', () => { + let indexer: ElasticSearchSearchEngineIndexer; + let bulkSpy: jest.Mock; + let catSpy: jest.Mock; + let createSpy: jest.Mock; + let aliasesSpy: jest.Mock; + let deleteSpy: jest.Mock; + + beforeEach(() => { + // Instantiate the indexer to be tested. + indexer = new ElasticSearchSearchEngineIndexer({ + type: 'some-type', + indexPrefix: '', + indexSeparator: '-index__', + alias: 'some-type-index__search', + logger: getVoidLogger(), + elasticSearchClient: client, + }); + + // Set up all requisite Elastic mocks. + mock.clearAll(); + bulkSpy = jest.fn().mockReturnValue({ took: 9, errors: false, items: [] }); + mock.add( + { + method: 'POST', + path: '/_bulk', + }, + bulkSpy, + ); + mock.add( + { + method: 'GET', + path: '/:index/_refresh', + }, + jest.fn().mockReturnValue({}), + ); + + catSpy = jest.fn().mockReturnValue([ + { + alias: 'some-type-index__search', + index: 'some-type-index__123tobedeleted', + filter: '-', + 'routing.index': '-', + 'routing.search': '-', + is_write_index: '-', + }, + ]); + mock.add( + { + method: 'GET', + path: '/_cat/aliases/some-type-index__search', + }, + catSpy, + ); + + createSpy = jest.fn().mockReturnValue({ + acknowledged: true, + shards_acknowledged: true, + index: 'single_index', + }); + mock.add( + { + method: 'PUT', + path: '/:index', + }, + createSpy, + ); + + aliasesSpy = jest.fn().mockReturnValue({}); + mock.add( + { + method: 'POST', + path: '*', + }, + aliasesSpy, + ); + + deleteSpy = jest.fn().mockReturnValue({}); + mock.add( + { + method: 'DELETE', + path: '/some-type-index__123tobedeleted', + }, + deleteSpy, + ); + }); + + it('indexes documents', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + }, + { + title: 'Another test', + text: 'Some more text', + location: 'test/location/2', + }, + ]; + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + // Older indices should have been queried for. + expect(catSpy).toHaveBeenCalled(); + + // A new index should have been created. + const createdIndex = createSpy.mock.calls[0][0].path.slice(1); + expect(createdIndex).toContain('some-type-index__'); + + // Bulk helper should have been called with documents. + const bulkBody = bulkSpy.mock.calls[0][0].body; + expect(bulkBody[0]).toStrictEqual({ index: { _index: createdIndex } }); + expect(bulkBody[1]).toStrictEqual(documents[0]); + expect(bulkBody[2]).toStrictEqual({ index: { _index: createdIndex } }); + expect(bulkBody[3]).toStrictEqual(documents[1]); + + // Alias should have been rotated. + expect(aliasesSpy).toHaveBeenCalled(); + const aliasActions = aliasesSpy.mock.calls[0][0].body.actions; + expect(aliasActions[0]).toStrictEqual({ + remove: { index: 'some-type-index__*', alias: 'some-type-index__search' }, + }); + expect(aliasActions[1]).toStrictEqual({ + add: { index: createdIndex, alias: 'some-type-index__search' }, + }); + + // Old index should be cleaned up. + expect(deleteSpy).toHaveBeenCalled(); + }); + + it('handles bulk and batching during indexing', async () => { + const documents = range(550).map(i => ({ + title: `Hello World ${i}`, + location: `location-${i}`, + // Generate large document sizes to trigger ES bulk flushing. + text: range(2000).join(', '), + })); + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + // Ensure multiple bulk requests were made. + expect(bulkSpy).toHaveBeenCalledTimes(2); + + // Ensure the first and last documents were included in the payloads. + const docLocations: string[] = [ + ...bulkSpy.mock.calls[0][0].body.map((l: any) => l.location), + ...bulkSpy.mock.calls[1][0].body.map((l: any) => l.location), + ]; + expect(docLocations).toContain('location-0'); + expect(docLocations).toContain('location-549'); + }); + + it('ignores cleanup when no existing indices exist', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + }, + ]; + + // Update initial alias cat to return nothing. + catSpy = jest.fn().mockReturnValue([]); + mock.clear({ + method: 'GET', + path: '/_cat/aliases/some-type-index__search', + }); + mock.add( + { + method: 'GET', + path: '/_cat/aliases/some-type-index__search', + }, + catSpy, + ); + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + // Final deletion shouldn't be called. + expect(deleteSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts new file mode 100644 index 0000000000..cb3674ddd1 --- /dev/null +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts @@ -0,0 +1,176 @@ +/* + * Copyright 2022 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 { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { Client } from '@elastic/elasticsearch'; +import { Readable } from 'stream'; +import { Logger } from 'winston'; + +export type ElasticSearchSearchEngineIndexerOptions = { + type: string; + indexPrefix: string; + indexSeparator: string; + alias: string; + logger: Logger; + elasticSearchClient: Client; +}; + +function duration(startTimestamp: [number, number]): string { + const delta = process.hrtime(startTimestamp); + const seconds = delta[0] + delta[1] / 1e9; + return `${seconds.toFixed(1)}s`; +} + +export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { + private received: number = 0; + private processed: number = 0; + private removableIndices: string[] = []; + + private readonly startTimestamp: [number, number]; + private readonly type: string; + public readonly indexName: string; + private readonly indexPrefix: string; + private readonly indexSeparator: string; + private readonly alias: string; + private readonly logger: Logger; + private readonly sourceStream: Readable; + private readonly elasticSearchClient: Client; + private bulkResult: Promise; + + constructor(options: ElasticSearchSearchEngineIndexerOptions) { + super({ batchSize: 100 }); + this.logger = options.logger; + this.startTimestamp = process.hrtime(); + this.type = options.type; + this.indexPrefix = options.indexPrefix; + this.indexSeparator = options.indexSeparator; + this.indexName = this.constructIndexName(`${Date.now()}`); + this.alias = options.alias; + this.elasticSearchClient = options.elasticSearchClient; + + // The ES client bulk helper supports stream-based indexing, but we have to + // supply the stream directly to it at instantiation-time. We can't supply + // this class itself, so instead, we create this inline stream instead. + this.sourceStream = new Readable({ objectMode: true }); + this.sourceStream._read = () => {}; + + // eslint-disable-next-line consistent-this + const that = this; + + // Keep a reference to the ES Bulk helper so that we can know when all + // documents have been successfully written to ES. + this.bulkResult = this.elasticSearchClient.helpers.bulk({ + datasource: this.sourceStream, + onDocument() { + that.processed++; + return { + index: { _index: that.indexName }, + }; + }, + refreshOnCompletion: that.indexName, + }); + } + + async initialize(): Promise { + this.logger.info(`Started indexing documents for index ${this.type}`); + + const aliases = await this.elasticSearchClient.cat.aliases({ + format: 'json', + name: this.alias, + }); + + this.removableIndices = aliases.body.map( + (r: Record) => r.index, + ); + + await this.elasticSearchClient.indices.create({ + index: this.indexName, + }); + } + + async index(documents: IndexableDocument[]): Promise { + await this.isReady(); + documents.forEach(document => { + this.received++; + this.sourceStream.push(document); + }); + } + + async finalize(): Promise { + // Wait for all documents to be processed. + await this.isReady(); + + // Close off the underlying stream connected to ES, indicating that no more + // documents will be written. + this.sourceStream.push(null); + + // Wait for the bulk helper to finish processing. + const result = await this.bulkResult; + + // Rotate aliases upon completion. Allow errors to bubble up so that we can + // clean up the create index. + this.logger.info( + `Indexing completed for index ${this.type} in ${duration( + this.startTimestamp, + )}`, + result, + ); + await this.elasticSearchClient.indices.updateAliases({ + body: { + actions: [ + { + remove: { index: this.constructIndexName('*'), alias: this.alias }, + }, + { add: { index: this.indexName, alias: this.alias } }, + ], + }, + }); + + // If any indices are removable, remove them. Do not bubble up this error, + // as doing so would delete the now aliased index. Log instead. + if (this.removableIndices.length) { + this.logger.info('Removing stale search indices', this.removableIndices); + try { + await this.elasticSearchClient.indices.delete({ + index: this.removableIndices, + }); + } catch (e) { + this.logger.warn(`Failed to remove stale search indices: ${e}`); + } + } + } + + /** + * Ensures that the number of documents sent over the wire to ES matches the + * number of documents this stream has received so far. This helps manage + * backpressure in other parts of the indexing pipeline. + */ + private isReady(): Promise { + return new Promise(resolve => { + const interval = setInterval(() => { + if (this.received === this.processed) { + clearInterval(interval); + resolve(); + } + }, 50); + }); + } + + private constructIndexName(postFix: string) { + return `${this.indexPrefix}${this.type}${this.indexSeparator}${postFix}`; + } +} diff --git a/plugins/search-backend-module-elasticsearch/src/engines/index.ts b/plugins/search-backend-module-elasticsearch/src/engines/index.ts index d5eee37803..19c24b37ff 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/index.ts @@ -19,3 +19,7 @@ export type { ConcreteElasticSearchQuery, ElasticSearchClientOptions, } from './ElasticSearchSearchEngine'; +export type { + ElasticSearchSearchEngineIndexer, + ElasticSearchSearchEngineIndexerOptions, +} from './ElasticSearchSearchEngineIndexer'; diff --git a/plugins/search-backend-module-elasticsearch/src/index.ts b/plugins/search-backend-module-elasticsearch/src/index.ts index 8cf96de858..223141be6b 100644 --- a/plugins/search-backend-module-elasticsearch/src/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/index.ts @@ -21,4 +21,8 @@ */ export { ElasticSearchSearchEngine } from './engines'; -export type { ElasticSearchClientOptions } from './engines'; +export type { + ElasticSearchClientOptions, + ElasticSearchSearchEngineIndexer, + ElasticSearchSearchEngineIndexerOptions, +} from './engines'; diff --git a/plugins/search-backend-module-pg/.eslintrc.js b/plugins/search-backend-module-pg/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/search-backend-module-pg/.eslintrc.js +++ b/plugins/search-backend-module-pg/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index 08986f6d34..8a710dccd9 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-search-backend-module-pg +## 0.3.1-next.0 + +### Patch Changes + +- 3e54f6c436: Use `@backstage/plugin-search-common` package instead of `@backstage/search-common`. +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-search-common@0.3.1-next.0 + - @backstage/plugin-search-backend-node@0.5.1-next.0 + +## 0.3.0 + +### Minor Changes + +- 022507c860: **BREAKING** + + The `PgSearchEngine` implements the new stream-based indexing process expected + by the latest `@backstage/search-backend-node`. + + When updating to this version, you must also update to the latest version of + `@backstage/search-backend-node`. Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for further details. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-search-backend-node@0.5.0 + - @backstage/search-common@0.3.0 + ## 0.2.9 ### Patch Changes diff --git a/plugins/search-backend-module-pg/api-report.md b/plugins/search-backend-module-pg/api-report.md index e2edfc291a..39847a4935 100644 --- a/plugins/search-backend-module-pg/api-report.md +++ b/plugins/search-backend-module-pg/api-report.md @@ -3,12 +3,13 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { IndexableDocument } from '@backstage/search-common'; +import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { Knex } from 'knex'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; -import { SearchQuery } from '@backstage/search-common'; -import { SearchResultSet } from '@backstage/search-common'; +import { SearchQuery } from '@backstage/plugin-search-common'; +import { SearchResultSet } from '@backstage/plugin-search-common'; // Warning: (ae-missing-release-tag) "ConcretePgSearchQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -28,6 +29,8 @@ export class DatabaseDocumentStore implements DatabaseStore { // (undocumented) static create(knex: Knex): Promise; // (undocumented) + getTransaction(): Promise; + // (undocumented) insertDocuments( tx: Knex.Transaction, type: string, @@ -55,6 +58,8 @@ export interface DatabaseStore { // (undocumented) completeInsert(tx: Knex.Transaction, type: string): Promise; // (undocumented) + getTransaction(): Promise; + // (undocumented) insertDocuments( tx: Knex.Transaction, type: string, @@ -81,7 +86,7 @@ export class PgSearchEngine implements SearchEngine { database: PluginDatabaseManager; }): Promise; // (undocumented) - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; // (undocumented) query(query: SearchQuery): Promise; // (undocumented) @@ -94,6 +99,28 @@ export class PgSearchEngine implements SearchEngine { translator(query: SearchQuery): ConcretePgSearchQuery; } +// Warning: (ae-missing-release-tag) "PgSearchEngineIndexer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class PgSearchEngineIndexer extends BatchSearchEngineIndexer { + constructor(options: PgSearchEngineIndexerOptions); + // (undocumented) + finalize(): Promise; + // (undocumented) + index(documents: IndexableDocument[]): Promise; + // (undocumented) + initialize(): Promise; +} + +// Warning: (ae-missing-release-tag) "PgSearchEngineIndexerOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PgSearchEngineIndexerOptions = { + batchSize: number; + type: string; + databaseStore: DatabaseStore; +}; + // Warning: (ae-missing-release-tag) "PgSearchQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index d006c4e675..60ada88666 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-pg", "description": "A module for the search backend that implements search using PostgreSQL", - "version": "0.2.9", + "version": "0.3.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,15 +23,15 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/search-common": "^0.2.4", - "@backstage/plugin-search-backend-node": "^0.4.7", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/plugin-search-backend-node": "^0.5.1-next.0", + "@backstage/plugin-search-common": "^0.3.1-next.0", "lodash": "^4.17.21", "knex": "^1.0.2" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.19", - "@backstage/cli": "^0.14.1" + "@backstage/backend-test-utils": "^0.1.21-next.0", + "@backstage/cli": "^0.15.2-next.0" }, "files": [ "dist", diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts index 56a3a6e643..8618bed4c8 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { range } from 'lodash'; import { DatabaseStore } from '../database'; import { ConcretePgSearchQuery, @@ -21,6 +20,13 @@ import { encodePageCursor, PgSearchEngine, } from './PgSearchEngine'; +import { PgSearchEngineIndexer } from './PgSearchEngineIndexer'; + +jest.mock('./PgSearchEngineIndexer', () => ({ + PgSearchEngineIndexer: jest + .fn() + .mockImplementation(async () => 'the-expected-indexer'), +})); describe('PgSearchEngine', () => { const tx: any = {} as any; @@ -30,6 +36,7 @@ describe('PgSearchEngine', () => { beforeEach(() => { database = { transaction: jest.fn(), + getTransaction: jest.fn(), insertDocuments: jest.fn(), query: jest.fn(), completeInsert: jest.fn(), @@ -122,46 +129,21 @@ describe('PgSearchEngine', () => { }); }); - describe('insert', () => { - it('should insert documents', async () => { - database.transaction.mockImplementation(fn => fn(tx)); + describe('index', () => { + it('should instantiate indexer', async () => { + const indexer = await searchEngine.getIndexer('my-type'); - const documents = [ - { title: 'Hello World', text: 'Lorem Ipsum', location: 'location-1' }, - { - location: 'location-2', - text: 'Hello World', - title: 'Dolor sit amet', - }, - ]; - - await searchEngine.index('my-type', documents); - - expect(database.transaction).toHaveBeenCalledTimes(1); - expect(database.prepareInsert).toHaveBeenCalledTimes(1); - expect(database.insertDocuments).toHaveBeenCalledWith( - tx, - 'my-type', - documents, + // Indexer instantiated with expected args. + expect(PgSearchEngineIndexer).toHaveBeenCalledWith( + expect.objectContaining({ + batchSize: 100, + type: 'my-type', + databaseStore: database, + }), ); - expect(database.completeInsert).toHaveBeenCalledWith(tx, 'my-type'); - }); - it('should batch insert documents', async () => { - database.transaction.mockImplementation(fn => fn(tx)); - - const documents = range(350).map(i => ({ - title: `Hello World ${i}`, - text: 'Lorem Ipsum', - location: `location-${i}`, - })); - - await searchEngine.index('my-type', documents); - - expect(database.transaction).toHaveBeenCalledTimes(1); - expect(database.prepareInsert).toHaveBeenCalledTimes(1); - expect(database.insertDocuments).toBeCalledTimes(4); - expect(database.completeInsert).toHaveBeenCalledWith(tx, 'my-type'); + // Indexer is as expected. + expect(indexer).toBe('the-expected-indexer'); }); }); diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts index 6fd6c571a8..8f75112837 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts @@ -15,12 +15,8 @@ */ import { PluginDatabaseManager } from '@backstage/backend-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; -import { - IndexableDocument, - SearchQuery, - SearchResultSet, -} from '@backstage/search-common'; -import { chunk } from 'lodash'; +import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common'; +import { PgSearchEngineIndexer } from './PgSearchEngineIndexer'; import { DatabaseDocumentStore, DatabaseStore, @@ -77,16 +73,11 @@ export class PgSearchEngine implements SearchEngine { this.translator = translator; } - async index(type: string, documents: IndexableDocument[]): Promise { - await this.databaseStore.transaction(async tx => { - await this.databaseStore.prepareInsert(tx); - - const batchSize = 100; - for (const documentBatch of chunk(documents, batchSize)) { - await this.databaseStore.insertDocuments(tx, type, documentBatch); - } - - await this.databaseStore.completeInsert(tx, type); + async getIndexer(type: string) { + return new PgSearchEngineIndexer({ + batchSize: 100, + type, + databaseStore: this.databaseStore, }); } diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts new file mode 100644 index 0000000000..1fc7e74fc4 --- /dev/null +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright 2022 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 { TestPipeline } from '@backstage/plugin-search-backend-node'; +import { range } from 'lodash'; +import { PgSearchEngineIndexer } from './PgSearchEngineIndexer'; +import { DatabaseStore } from '../database'; + +describe('PgSearchEngineIndexer', () => { + const tx = { + rollback: jest.fn(), + commit: jest.fn(), + } as any; + let database: jest.Mocked; + let indexer: PgSearchEngineIndexer; + + beforeEach(() => { + jest.clearAllMocks(); + database = { + transaction: jest.fn().mockImplementation(fn => fn(tx)), + getTransaction: jest.fn().mockReturnValue(tx), + insertDocuments: jest.fn(), + query: jest.fn(), + completeInsert: jest.fn(), + prepareInsert: jest.fn(), + }; + indexer = new PgSearchEngineIndexer({ + batchSize: 100, + type: 'my-type', + databaseStore: database, + }); + }); + + it('should insert documents', async () => { + const documents = [ + { title: 'Hello World', text: 'Lorem Ipsum', location: 'location-1' }, + { + location: 'location-2', + text: 'Hello World', + title: 'Dolor sit amet', + }, + ]; + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.prepareInsert).toHaveBeenCalledTimes(1); + expect(database.insertDocuments).toHaveBeenCalledWith( + tx, + 'my-type', + documents, + ); + expect(database.completeInsert).toHaveBeenCalledWith(tx, 'my-type'); + expect(tx.commit).toHaveBeenCalled(); + }); + + it('should batch insert documents', async () => { + const documents = range(350).map(i => ({ + title: `Hello World ${i}`, + text: 'Lorem Ipsum', + location: `location-${i}`, + })); + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.prepareInsert).toHaveBeenCalledTimes(1); + expect(database.insertDocuments).toBeCalledTimes(4); + expect(database.completeInsert).toHaveBeenCalledWith(tx, 'my-type'); + }); + + it('should close out stream and bubble up error on prepare', async () => { + const expectedError = new Error('Prepare error'); + const documents = [ + { + title: `Hello World`, + text: 'Lorem Ipsum', + location: `location`, + }, + ]; + + database.prepareInsert.mockRejectedValueOnce(expectedError); + const result = await TestPipeline.withSubject(indexer) + .withDocuments(documents) + .execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.insertDocuments).not.toHaveBeenCalled(); + expect(database.completeInsert).not.toHaveBeenCalled(); + expect(result.error).toBe(expectedError); + expect(tx.rollback).toHaveBeenCalledWith(expectedError); + }); + + it('should close tx and bubble up error on insert', async () => { + const expectedError = new Error('Index error'); + const documents = [ + { + title: `Hello World`, + text: 'Lorem Ipsum', + location: `location`, + }, + ]; + + database.insertDocuments.mockRejectedValueOnce(expectedError); + const result = await TestPipeline.withSubject(indexer) + .withDocuments(documents) + .execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.prepareInsert).toHaveBeenCalledTimes(1); + expect(database.completeInsert).not.toHaveBeenCalled(); + expect(result.error).toBe(expectedError); + expect(tx.rollback).toHaveBeenCalledWith(expectedError); + }); + + it('should close tx and bubble up error on completion', async () => { + const expectedError = new Error('Completion error'); + const documents = [ + { + title: `Hello World`, + text: 'Lorem Ipsum', + location: `location`, + }, + ]; + + database.completeInsert.mockRejectedValueOnce(expectedError); + const result = await TestPipeline.withSubject(indexer) + .withDocuments(documents) + .execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.prepareInsert).toHaveBeenCalledTimes(1); + expect(database.insertDocuments).toHaveBeenCalledTimes(1); + expect(database.completeInsert).toHaveBeenCalledTimes(1); + expect(result.error).toBe(expectedError); + expect(tx.rollback).toHaveBeenCalledWith(expectedError); + }); +}); diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts new file mode 100644 index 0000000000..fa27509a69 --- /dev/null +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2022 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 { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { Knex } from 'knex'; +import { DatabaseStore } from '../database'; + +export type PgSearchEngineIndexerOptions = { + batchSize: number; + type: string; + databaseStore: DatabaseStore; +}; + +export class PgSearchEngineIndexer extends BatchSearchEngineIndexer { + private store: DatabaseStore; + private type: string; + private tx: Knex.Transaction | undefined; + + constructor(options: PgSearchEngineIndexerOptions) { + super({ batchSize: options.batchSize }); + this.store = options.databaseStore; + this.type = options.type; + } + + async initialize(): Promise { + this.tx = await this.store.getTransaction(); + try { + await this.store.prepareInsert(this.tx); + } catch (e) { + // In case of error, rollback the transaction and re-throw the error so + // that the stream can be closed and destroyed properly. + this.tx.rollback(e); + throw e; + } + } + + async index(documents: IndexableDocument[]): Promise { + try { + await this.store.insertDocuments(this.tx!, this.type, documents); + } catch (e) { + // In case of error, rollback the transaction and re-throw the error so + // that the stream can be closed and destroyed properly. + this.tx!.rollback(e); + throw e; + } + } + + async finalize(): Promise { + // Attempt to complete and commit the transaction. + try { + await this.store.completeInsert(this.tx!, this.type); + this.tx!.commit(); + } catch (e) { + // Otherwise, rollback the transaction and re-throw the error so that the + // stream can be closed and destroyed properly. + this.tx!.rollback!(e); + throw e; + } + } +} diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/index.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/index.ts index 7994998baf..7f8e297648 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/index.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/index.ts @@ -15,3 +15,7 @@ */ export { PgSearchEngine } from './PgSearchEngine'; export type { ConcretePgSearchQuery } from './PgSearchEngine'; +export type { + PgSearchEngineIndexer, + PgSearchEngineIndexerOptions, +} from './PgSearchEngineIndexer'; diff --git a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.test.ts b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.test.ts index 71c012ec00..b973c2e14f 100644 --- a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.test.ts +++ b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { DatabaseDocumentStore } from './DatabaseDocumentStore'; describe('DatabaseDocumentStore', () => { diff --git a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts index 8c180ea019..33b2d0c0d6 100644 --- a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts +++ b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { resolvePackagePath } from '@backstage/backend-common'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { Knex } from 'knex'; import { DatabaseStore, @@ -71,6 +71,10 @@ export class DatabaseDocumentStore implements DatabaseStore { return await this.db.transaction(fn); } + async getTransaction(): Promise { + return this.db.transaction(); + } + async prepareInsert(tx: Knex.Transaction): Promise { // We create a temporary table to collect the hashes of the documents that // we expect to be in the documents table at the end. The table is deleted diff --git a/plugins/search-backend-module-pg/src/database/types.ts b/plugins/search-backend-module-pg/src/database/types.ts index 0c0596160e..7e87658745 100644 --- a/plugins/search-backend-module-pg/src/database/types.ts +++ b/plugins/search-backend-module-pg/src/database/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { Knex } from 'knex'; export interface PgSearchQuery { @@ -26,6 +26,7 @@ export interface PgSearchQuery { export interface DatabaseStore { transaction(fn: (tx: Knex.Transaction) => Promise): Promise; + getTransaction(): Promise; prepareInsert(tx: Knex.Transaction): Promise; insertDocuments( tx: Knex.Transaction, diff --git a/plugins/search-backend-node/.eslintrc.js b/plugins/search-backend-node/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/search-backend-node/.eslintrc.js +++ b/plugins/search-backend-node/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 864376a7e7..e13ed7dc4d 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-search-backend-node +## 0.5.1-next.0 + +### Patch Changes + +- 3e54f6c436: Use `@backstage/plugin-search-common` package instead of `@backstage/search-common`. +- Updated dependencies + - @backstage/plugin-search-common@0.3.1-next.0 + +## 0.5.0 + +### Minor Changes + +- 022507c860: **BREAKING** + + The Backstage Search Platform's indexing process has been rewritten as a stream + pipeline in order to improve efficiency and performance on large document sets. + + The concepts of `Collator` and `Decorator` have been replaced with readable and + transform object streams (respectively), as well as factory classes to + instantiate them. Accordingly, the `SearchEngine.index()` method has also been + replaced with a `getIndexer()` factory method that resolves to a writable + object stream. + + Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for further details. + +### Patch Changes + +- Updated dependencies + - @backstage/search-common@0.3.0 + ## 0.4.7 ### Patch Changes diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md index 1929009760..e4359eafac 100644 --- a/plugins/search-backend-node/api-report.md +++ b/plugins/search-backend-node/api-report.md @@ -3,30 +3,60 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { DocumentCollator } from '@backstage/search-common'; -import { DocumentDecorator } from '@backstage/search-common'; -import { DocumentTypeInfo } from '@backstage/search-common'; -import { IndexableDocument } from '@backstage/search-common'; -import { Logger as Logger_2 } from 'winston'; -import { default as lunr_2 } from 'lunr'; -import { QueryTranslator } from '@backstage/search-common'; -import { SearchEngine } from '@backstage/search-common'; -import { SearchQuery } from '@backstage/search-common'; -import { SearchResultSet } from '@backstage/search-common'; +/// -// Warning: (ae-missing-release-tag) "IndexBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { DocumentDecoratorFactory } from '@backstage/plugin-search-common'; +import { DocumentTypeInfo } from '@backstage/plugin-search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { Logger } from 'winston'; +import { default as lunr_2 } from 'lunr'; +import { QueryTranslator } from '@backstage/plugin-search-common'; +import { Readable } from 'stream'; +import { SearchEngine } from '@backstage/plugin-search-common'; +import { SearchQuery } from '@backstage/plugin-search-common'; +import { SearchResultSet } from '@backstage/plugin-search-common'; +import { Transform } from 'stream'; +import { Writable } from 'stream'; + +// @beta +export abstract class BatchSearchEngineIndexer extends Writable { + constructor(options: BatchSearchEngineOptions); + abstract finalize(): Promise; + abstract index(documents: IndexableDocument[]): Promise; + abstract initialize(): Promise; +} + +// @beta (undocumented) +export type BatchSearchEngineOptions = { + batchSize: number; +}; + +// @beta (undocumented) +export type ConcreteLunrQuery = { + lunrQueryBuilder: lunr_2.Index.QueryBuilder; + documentTypes?: string[]; + pageSize: number; +}; + +// @beta +export abstract class DecoratorBase extends Transform { + constructor(); + abstract decorate( + document: IndexableDocument, + ): Promise; + abstract finalize(): Promise; + abstract initialize(): Promise; +} + +// @beta (undocumented) export class IndexBuilder { - // Warning: (ae-forgotten-export) The symbol "IndexBuilderOptions" needs to be exported by the entry point index.d.ts constructor({ logger, searchEngine }: IndexBuilderOptions); - // Warning: (ae-forgotten-export) The symbol "RegisterCollatorParameters" needs to be exported by the entry point index.d.ts addCollator({ - collator, + factory, defaultRefreshIntervalSeconds, }: RegisterCollatorParameters): void; - // Warning: (ae-forgotten-export) The symbol "RegisterDecoratorParameters" needs to be exported by the entry point index.d.ts - addDecorator({ decorator }: RegisterDecoratorParameters): void; + addDecorator({ factory }: RegisterDecoratorParameters): void; build(): Promise<{ scheduler: Scheduler; }>; @@ -36,38 +66,80 @@ export class IndexBuilder { getSearchEngine(): SearchEngine; } -// Warning: (ae-missing-release-tag) "LunrSearchEngine" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta (undocumented) +export type IndexBuilderOptions = { + searchEngine: SearchEngine; + logger: Logger; +}; + +// @beta (undocumented) +export type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery; + +// @beta (undocumented) export class LunrSearchEngine implements SearchEngine { - constructor({ logger }: { logger: Logger_2 }); + constructor({ logger }: { logger: Logger }); // (undocumented) protected docStore: Record; // (undocumented) - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; // (undocumented) - protected logger: Logger_2; + protected logger: Logger; // (undocumented) protected lunrIndices: Record; // (undocumented) query(query: SearchQuery): Promise; - // Warning: (ae-forgotten-export) The symbol "LunrQueryTranslator" needs to be exported by the entry point index.d.ts - // // (undocumented) setTranslator(translator: LunrQueryTranslator): void; // (undocumented) protected translator: QueryTranslator; } -// Warning: (ae-missing-release-tag) "Scheduler" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta (undocumented) +export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer { + constructor(); + // (undocumented) + buildIndex(): lunr_2.Index; + // (undocumented) + finalize(): Promise; + // (undocumented) + getDocumentStore(): Record; + // (undocumented) + index(documents: IndexableDocument[]): Promise; + // (undocumented) + initialize(): Promise; +} + +// @beta +export interface RegisterCollatorParameters { + defaultRefreshIntervalSeconds: number; + factory: DocumentCollatorFactory; +} + +// @beta +export interface RegisterDecoratorParameters { + factory: DocumentDecoratorFactory; +} + +// @beta (undocumented) export class Scheduler { - constructor({ logger }: { logger: Logger_2 }); + constructor({ logger }: { logger: Logger }); addToSchedule(task: Function, interval: number): void; start(): void; stop(): void; } export { SearchEngine }; + +// @beta +export class TestPipeline { + execute(): Promise; + withDocuments(documents: IndexableDocument[]): TestPipeline; + static withSubject(subject: Readable | Transform | Writable): TestPipeline; +} + +// @beta +export type TestPipelineResult = { + error: unknown; + documents: IndexableDocument[]; +}; ``` diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index dd8ef5599a..bff24ad52d 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-node", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", - "version": "0.4.7", + "version": "0.5.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,14 +23,16 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/search-common": "^0.2.4", - "winston": "^3.2.1", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-search-common": "^0.3.1-next.0", + "@types/lunr": "^2.3.3", + "lodash": "^4.17.21", "lunr": "^2.3.9", - "@types/lunr": "^2.3.3" + "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/cli": "^0.14.1" + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/cli": "^0.15.2-next.0" }, "files": [ "dist" diff --git a/plugins/search-backend-node/src/IndexBuilder.test.ts b/plugins/search-backend-node/src/IndexBuilder.test.ts index 0465f2701c..6a32343205 100644 --- a/plugins/search-backend-node/src/IndexBuilder.test.ts +++ b/plugins/search-backend-node/src/IndexBuilder.test.ts @@ -16,35 +16,37 @@ import { getVoidLogger } from '@backstage/backend-common'; import { - DocumentCollator, - DocumentDecorator, - IndexableDocument, -} from '@backstage/search-common'; + DocumentCollatorFactory, + DocumentDecoratorFactory, +} from '@backstage/plugin-search-common'; +import { Readable, Transform } from 'stream'; import { IndexBuilder } from './IndexBuilder'; import { LunrSearchEngine, SearchEngine } from './index'; -class TestDocumentCollator implements DocumentCollator { +class TestDocumentCollatorFactory implements DocumentCollatorFactory { readonly type: string = 'anything'; - async execute(): Promise { - return []; + async getCollator(): Promise { + const collator = new Readable({ objectMode: true }); + collator._read = () => {}; + return collator; } } -class TypedDocumentCollator extends TestDocumentCollator { +class TypedDocumentCollatorFactory extends TestDocumentCollatorFactory { readonly type = 'an-expected-type'; } -class TestDocumentDecorator implements DocumentDecorator { - async execute(documents: IndexableDocument[]) { - return documents; +class TestDocumentDecoratorFactory implements DocumentDecoratorFactory { + async getDecorator(): Promise { + return new Transform(); } } -class TypedDocumentDecorator extends TestDocumentDecorator { +class TypedDocumentDecoratorFactory extends TestDocumentDecoratorFactory { readonly types = ['an-expected-type']; } -class DifferentlyTypedDocumentDecorator extends TestDocumentDecorator { +class DifferentlyTypedDocumentDecoratorFactory extends TestDocumentDecoratorFactory { readonly types = ['not-the-expected-type']; } @@ -64,13 +66,13 @@ describe('IndexBuilder', () => { describe('addCollator', () => { it('adds a collator', async () => { jest.useFakeTimers(); - const testCollator = new TestDocumentCollator(); - const collatorSpy = jest.spyOn(testCollator, 'execute'); + const testCollatorFactory = new TestDocumentCollatorFactory(); + const collatorSpy = jest.spyOn(testCollatorFactory, 'getCollator'); // Add a collator. testIndexBuilder.addCollator({ defaultRefreshIntervalSeconds: 6, - collator: testCollator, + factory: testCollatorFactory, }); // Build the index and ensure the collator was invoked. @@ -84,19 +86,19 @@ describe('IndexBuilder', () => { describe('addDecorator', () => { it('adds a decorator', async () => { jest.useFakeTimers(); - const testCollator = new TestDocumentCollator(); - const testDecorator = new TestDocumentDecorator(); - const decoratorSpy = jest.spyOn(testDecorator, 'execute'); + const testCollatorFactory = new TestDocumentCollatorFactory(); + const testDecoratorFactory = new TestDocumentDecoratorFactory(); + const decoratorSpy = jest.spyOn(testDecoratorFactory, 'getDecorator'); // Add a collator. testIndexBuilder.addCollator({ defaultRefreshIntervalSeconds: 6, - collator: testCollator, + factory: testCollatorFactory, }); // Add a decorator. testIndexBuilder.addDecorator({ - decorator: testDecorator, + factory: testDecoratorFactory, }); // Build the index and ensure the decorator was invoked. @@ -110,27 +112,20 @@ describe('IndexBuilder', () => { it('adds a type-specific decorator', async () => { jest.useFakeTimers(); - const testCollator = new TypedDocumentCollator(); - const testDecorator = new TypedDocumentDecorator(); - const docFixture = { - title: 'Test', - text: 'Test text.', - location: '/test/location', - }; - jest - .spyOn(testCollator, 'execute') - .mockImplementation(async () => [docFixture]); - const decoratorSpy = jest.spyOn(testDecorator, 'execute'); + const testCollatorFactory = new TypedDocumentCollatorFactory(); + const testDecoratorFactory = new TypedDocumentDecoratorFactory(); + jest.spyOn(testCollatorFactory, 'getCollator'); + const decoratorSpy = jest.spyOn(testDecoratorFactory, 'getDecorator'); // Add a collator. testIndexBuilder.addCollator({ defaultRefreshIntervalSeconds: 6, - collator: testCollator, + factory: testCollatorFactory, }); // Add a decorator for the same type. testIndexBuilder.addDecorator({ - decorator: testDecorator, + factory: testDecoratorFactory, }); // Build the index and ensure the decorator was invoked. @@ -140,31 +135,24 @@ describe('IndexBuilder', () => { // wait for async decorator execution await Promise.resolve(); expect(decoratorSpy).toHaveBeenCalled(); - expect(decoratorSpy).toHaveBeenCalledWith([docFixture]); }); it('adds a type-specific decorator that should not be called', async () => { - const docFixture = { - title: 'Test', - text: 'Test text.', - location: '/test/location', - }; - const testCollator = new TestDocumentCollator(); - const testDecorator = new DifferentlyTypedDocumentDecorator(); - const collatorSpy = jest - .spyOn(testCollator, 'execute') - .mockImplementation(async () => [docFixture]); - const decoratorSpy = jest.spyOn(testDecorator, 'execute'); + const testCollatorFactory = new TestDocumentCollatorFactory(); + const testDecoratorFactory = + new DifferentlyTypedDocumentDecoratorFactory(); + const collatorSpy = jest.spyOn(testCollatorFactory, 'getCollator'); + const decoratorSpy = jest.spyOn(testDecoratorFactory, 'getDecorator'); // Add a collator. testIndexBuilder.addCollator({ defaultRefreshIntervalSeconds: 6, - collator: testCollator, + factory: testCollatorFactory, }); // Add a decorator for a different type. testIndexBuilder.addDecorator({ - decorator: testDecorator, + factory: testDecoratorFactory, }); // Build the index and ensure the decorator was not invoked. diff --git a/plugins/search-backend-node/src/IndexBuilder.ts b/plugins/search-backend-node/src/IndexBuilder.ts index 92adb03d7d..6be4489127 100644 --- a/plugins/search-backend-node/src/IndexBuilder.ts +++ b/plugins/search-backend-node/src/IndexBuilder.ts @@ -15,32 +15,31 @@ */ import { - DocumentCollator, - DocumentDecorator, + DocumentCollatorFactory, + DocumentDecoratorFactory, DocumentTypeInfo, - IndexableDocument, SearchEngine, -} from '@backstage/search-common'; +} from '@backstage/plugin-search-common'; +import { Transform, pipeline } from 'stream'; import { Logger } from 'winston'; import { Scheduler } from './index'; import { + IndexBuilderOptions, RegisterCollatorParameters, RegisterDecoratorParameters, } from './types'; interface CollatorEnvelope { - collate: DocumentCollator; + factory: DocumentCollatorFactory; refreshInterval: number; } -type IndexBuilderOptions = { - searchEngine: SearchEngine; - logger: Logger; -}; - +/** + * @beta + */ export class IndexBuilder { private collators: Record; - private decorators: Record; + private decorators: Record; private documentTypes: Record; private searchEngine: SearchEngine; private logger: Logger; @@ -66,18 +65,18 @@ export class IndexBuilder { * given refresh interval. */ addCollator({ - collator, + factory, defaultRefreshIntervalSeconds, }: RegisterCollatorParameters): void { this.logger.info( - `Added ${collator.constructor.name} collator for type ${collator.type}`, + `Added ${factory.constructor.name} collator factory for type ${factory.type}`, ); - this.collators[collator.type] = { + this.collators[factory.type] = { refreshInterval: defaultRefreshIntervalSeconds, - collate: collator, + factory, }; - this.documentTypes[collator.type] = { - visibilityPermission: collator.visibilityPermission, + this.documentTypes[factory.type] = { + visibilityPermission: factory.visibilityPermission, }; } @@ -86,18 +85,18 @@ export class IndexBuilder { * the decorator, it will be applied to documents from all known collators, * otherwise it will only be applied to documents of the given types. */ - addDecorator({ decorator }: RegisterDecoratorParameters): void { - const types = decorator.types || ['*']; + addDecorator({ factory }: RegisterDecoratorParameters): void { + const types = factory.types || ['*']; this.logger.info( - `Added decorator ${decorator.constructor.name} to types ${types.join( + `Added decorator ${factory.constructor.name} to types ${types.join( ', ', )}`, ); types.forEach(type => { if (this.decorators.hasOwnProperty(type)) { - this.decorators[type].push(decorator); + this.decorators[type].push(factory); } else { - this.decorators[type] = [decorator]; + this.decorators[type] = [factory]; } }); } @@ -111,46 +110,43 @@ export class IndexBuilder { Object.keys(this.collators).forEach(type => { scheduler.addToSchedule(async () => { - // Collate, Decorate, Index. - const decorators: DocumentDecorator[] = ( - this.decorators['*'] || [] - ).concat(this.decorators[type] || []); - - this.logger.debug( - `Collating documents for ${type} via ${this.collators[type].collate.constructor.name}`, + // Instantiate the collator. + const collator = await this.collators[type].factory.getCollator(); + this.logger.info( + `Collating documents for ${type} via ${this.collators[type].factory.constructor.name}`, ); - let documents: IndexableDocument[]; - try { - documents = await this.collators[type].collate.execute(); - } catch (e) { - this.logger.error( - `Collating documents for ${type} via ${this.collators[type].collate.constructor.name} failed: ${e}`, - ); - return; - } + // Instantiate all relevant decorators. + const decorators: Transform[] = await Promise.all( + (this.decorators['*'] || []) + .concat(this.decorators[type] || []) + .map(async factory => { + const decorator = await factory.getDecorator(); + this.logger.info( + `Attached decorator via ${factory.constructor.name} to ${type} index pipeline.`, + ); + return decorator; + }), + ); - for (let i = 0; i < decorators.length; i++) { - this.logger.debug( - `Decorating ${type} documents via ${decorators[i].constructor.name}`, - ); - try { - documents = await decorators[i].execute(documents); - } catch (e) { - this.logger.error( - `Decorating ${type} documents via ${decorators[i].constructor.name} failed: ${e}`, - ); - return; - } - } + // Instantiate the indexer. + const indexer = await this.searchEngine.getIndexer(type); - if (!documents || documents.length === 0) { - this.logger.debug(`No documents for type "${type}" to index`); - return; - } + // Compose collator/decorators/indexer into a pipeline + return new Promise(done => { + pipeline([collator, ...decorators, indexer], error => { + if (error) { + this.logger.error( + `Collating documents for ${type} failed: ${error}`, + ); + } else { + this.logger.info(`Collating documents for ${type} succeeded`); + } - // pushing documents to index to a configured search engine. - await this.searchEngine.index(type, documents); + // Signal index pipeline completion! + done(); + }); + }); }, this.collators[type].refreshInterval * 1000); }); diff --git a/plugins/search-backend-node/src/Scheduler.ts b/plugins/search-backend-node/src/Scheduler.ts index 3e356aa6aa..6debaa3dcd 100644 --- a/plugins/search-backend-node/src/Scheduler.ts +++ b/plugins/search-backend-node/src/Scheduler.ts @@ -26,6 +26,9 @@ type TaskEnvelope = { * TODO: coordination, error handling */ +/** + * @beta + */ export class Scheduler { private logger: Logger; private schedule: TaskEnvelope[]; diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts index cbe48ef5a7..46aaf47c60 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts @@ -16,28 +16,64 @@ import { getVoidLogger } from '@backstage/backend-common'; import lunr from 'lunr'; -import { SearchEngine } from '@backstage/search-common'; +import { + IndexableDocument, + SearchEngine, +} from '@backstage/plugin-search-common'; import { ConcreteLunrQuery, LunrSearchEngine, decodePageCursor, encodePageCursor, } from './LunrSearchEngine'; +import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; +import { TestPipeline } from '../test-utils'; /** * Just used to test the default translator shipped with LunrSearchEngine. */ -class LunrSearchEngineForTranslatorTests extends LunrSearchEngine { +class LunrSearchEngineForTests extends LunrSearchEngine { + getDocStore() { + return this.docStore; + } + setDocStore(docStore: Record) { + this.docStore = docStore; + } + getLunrIndices() { + return this.lunrIndices; + } getTranslator() { return this.translator; } } +const indexerMock = { + on: jest.fn(), + buildIndex: jest.fn(), + getDocumentStore: jest.fn(), +}; +jest.mock('./LunrSearchEngineIndexer', () => ({ + LunrSearchEngineIndexer: jest.fn().mockImplementation(() => indexerMock), +})); + +const getActualIndexer = (engine: SearchEngine, index: string) => { + (LunrSearchEngineIndexer as unknown as jest.Mock).mockImplementationOnce( + () => { + const ActualIndexer = jest.requireActual( + './LunrSearchEngineIndexer', + ).LunrSearchEngineIndexer; + return new ActualIndexer(); + }, + ); + return engine.getIndexer(index); +}; + describe('LunrSearchEngine', () => { let testLunrSearchEngine: SearchEngine; beforeEach(() => { testLunrSearchEngine = new LunrSearchEngine({ logger: getVoidLogger() }); + jest.clearAllMocks(); }); describe('translator', () => { @@ -65,7 +101,7 @@ describe('LunrSearchEngine', () => { }); it('should return translated query', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -107,7 +143,7 @@ describe('LunrSearchEngine', () => { }); it('should have default offset and limit', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -148,7 +184,7 @@ describe('LunrSearchEngine', () => { }); it('should return translated query with 1 filter', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -193,7 +229,7 @@ describe('LunrSearchEngine', () => { }); it('should handle single-item array filter as scalar value', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -224,7 +260,7 @@ describe('LunrSearchEngine', () => { }); it('should return translated query with multiple filters', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -273,7 +309,7 @@ describe('LunrSearchEngine', () => { }); it('should throw if translated query references missing field', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -334,7 +370,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -359,7 +401,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -392,7 +440,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -424,7 +478,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -456,7 +516,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -489,7 +555,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -522,7 +594,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -560,7 +638,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 2 documents - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -604,8 +688,21 @@ describe('LunrSearchEngine', () => { ]; // Mock 2 indices with 1 document each - await testLunrSearchEngine.index('test-index', mockDocuments); - await testLunrSearchEngine.index('test-index-2', mockDocuments2); + const indexer1 = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + const indexer2 = await getActualIndexer( + testLunrSearchEngine, + 'test-index-2', + ); + await TestPipeline.withSubject(indexer1) + .withDocuments(mockDocuments) + .execute(); + await TestPipeline.withSubject(indexer2) + .withDocuments(mockDocuments2) + .execute(); + // Perform search query scoped to "test-index-2" with a filter on the field "extraField" const mockedSearchResult = await testLunrSearchEngine.query({ term: 'testTitle', @@ -642,7 +739,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 2 documents - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -695,8 +798,20 @@ describe('LunrSearchEngine', () => { ]; // Mock 2 indices with 2 documents each - await testLunrSearchEngine.index('test-index', mockDocuments); - await testLunrSearchEngine.index('test-index-2', mockDocuments2); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); + const indexer2 = await getActualIndexer( + testLunrSearchEngine, + 'test-index-2', + ); + await TestPipeline.withSubject(indexer2) + .withDocuments(mockDocuments2) + .execute(); // Perform search query scoped to "test-index-2" const mockedSearchResult = await testLunrSearchEngine.query({ @@ -734,7 +849,13 @@ describe('LunrSearchEngine', () => { location: `test/location/${i}`, })); - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); const mockedSearchResult = await testLunrSearchEngine.query({ term: 'testTitle', @@ -767,7 +888,10 @@ describe('LunrSearchEngine', () => { location: `test/location/${i}`, })); - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer(testLunrSearchEngine, 'test-index'); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); const mockedSearchResult = await testLunrSearchEngine.query({ term: 'testTitle', @@ -793,22 +917,46 @@ describe('LunrSearchEngine', () => { }); describe('index', () => { - it('should index document', async () => { - const indexSpy = jest.spyOn(testLunrSearchEngine, 'index'); - const mockDocuments = [ - { - title: 'testTerm', - text: 'testText', - location: 'test/location', - }, - ]; + it('should get indexer', async () => { + const indexer = await testLunrSearchEngine.getIndexer('test-index'); + expect(LunrSearchEngineIndexer).toHaveBeenCalled(); + expect(indexer.on).toHaveBeenCalledWith('close', expect.any(Function)); + }); - // call index func and ensure the index func was invoked. - await testLunrSearchEngine.index('test-index', mockDocuments); - expect(indexSpy).toHaveBeenCalled(); - expect(indexSpy).toHaveBeenCalledWith('test-index', [ - { title: 'testTerm', text: 'testText', location: 'test/location' }, - ]); + it('should manage indices and docs on close', async () => { + const doc = { title: 'A doc', text: 'test', location: 'some-location' }; + + // Set up an inspectable search engine to pre-set some data. + const inspectableSearchEngine = new LunrSearchEngineForTests({ + logger: getVoidLogger(), + }); + inspectableSearchEngine.setDocStore({ 'existing-location': doc }); + + // Mock methds called by close handler. + indexerMock.buildIndex.mockReturnValueOnce('expected-index'); + indexerMock.getDocumentStore.mockReturnValueOnce({ + 'new-location': doc, + }); + + // Get the indexer and invoke its close handler. + await inspectableSearchEngine.getIndexer('test-index'); + const onClose = indexerMock.on.mock.calls[0][1] as Function; + onClose(); + + // Ensure mocked methods were called. + expect(indexerMock.buildIndex).toHaveBeenCalled(); + expect(indexerMock.getDocumentStore).toHaveBeenCalled(); + + // Ensure the lunr index was written to the search engine. + expect(inspectableSearchEngine.getLunrIndices()).toStrictEqual({ + 'test-index': 'expected-index', + }); + + // Ensure documents are merged into the existing store. + expect(inspectableSearchEngine.getDocStore()).toStrictEqual({ + 'existing-location': doc, + 'new-location': doc, + }); }); }); }); diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index ea51ffe8e1..b642647466 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -20,10 +20,14 @@ import { SearchResultSet, QueryTranslator, SearchEngine, -} from '@backstage/search-common'; +} from '@backstage/plugin-search-common'; import lunr from 'lunr'; import { Logger } from 'winston'; +import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; +/** + * @beta + */ export type ConcreteLunrQuery = { lunrQueryBuilder: lunr.Index.QueryBuilder; documentTypes?: string[]; @@ -35,8 +39,14 @@ type LunrResultEnvelope = { type: string; }; -type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery; +/** + * @beta + */ +export type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery; +/** + * @beta + */ export class LunrSearchEngine implements SearchEngine { protected lunrIndices: Record = {}; protected docStore: Record; @@ -124,30 +134,17 @@ export class LunrSearchEngine implements SearchEngine { this.translator = translator; } - async index(type: string, documents: IndexableDocument[]): Promise { - const lunrBuilder = new lunr.Builder(); + async getIndexer(type: string) { + const indexer = new LunrSearchEngineIndexer(); - lunrBuilder.pipeline.add(lunr.trimmer, lunr.stopWordFilter, lunr.stemmer); - lunrBuilder.searchPipeline.add(lunr.stemmer); - - // Make this lunr index aware of all relevant fields. - Object.keys(documents[0]).forEach(field => { - lunrBuilder.field(field); + indexer.on('close', () => { + // Once the stream is closed, build the index and store the documents in + // memory for later retrieval. + this.lunrIndices[type] = indexer.buildIndex(); + this.docStore = { ...this.docStore, ...indexer.getDocumentStore() }; }); - // Set "location" field as reference field - lunrBuilder.ref('location'); - - documents.forEach((document: IndexableDocument) => { - // Add document to Lunar index - lunrBuilder.add(document); - // Store documents in memory to be able to look up document using the ref during query time - // This is not how you should implement your SearchEngine implementation! Do not copy! - this.docStore[document.location] = document; - }); - - // "Rotate" the index by simply overwriting any existing index of the same name. - this.lunrIndices[type] = lunrBuilder.build(); + return indexer; } async query(query: SearchQuery): Promise { diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts new file mode 100644 index 0000000000..fbb6b153d0 --- /dev/null +++ b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2022 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 lunr from 'lunr'; +import { range } from 'lodash'; +import { TestPipeline } from '../test-utils'; +import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; + +const lunrBuilderAddSpy = jest.fn(); +const lunrBuilderRefSpy = jest.fn(); +const lunrBuilderFieldSpy = jest.fn(); +const lunrBuilderPipelineAddSpy = jest.fn(); +const lunrBuilderSearchPipelineAddSpy = jest.fn(); + +jest.mock('lunr', () => { + const actualLunr = jest.requireActual('lunr'); + return { + ...actualLunr, + Builder: jest.fn().mockImplementation(() => { + const actualBuilder = new actualLunr.Builder(); + actualBuilder.add = lunrBuilderAddSpy; + actualBuilder.ref = lunrBuilderRefSpy; + actualBuilder.field = lunrBuilderFieldSpy; + actualBuilder.pipeline.add = lunrBuilderPipelineAddSpy; + actualBuilder.searchPipeline.add = lunrBuilderSearchPipelineAddSpy; + return actualBuilder; + }), + }; +}); + +describe('LunrSearchEngineIndexer', () => { + let indexer: LunrSearchEngineIndexer; + + beforeEach(() => { + jest.clearAllMocks(); + indexer = new LunrSearchEngineIndexer(); + }); + + it('should index documents', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + }, + ]; + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + expect(lunrBuilderAddSpy).toHaveBeenCalledWith(documents[0]); + }); + + it('should index documents in bulk', async () => { + const documents = range(350).map(i => ({ + title: `Hello World ${i}`, + text: 'Lorem Ipsum', + location: `location-${i}`, + })); + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + expect(lunrBuilderAddSpy).toHaveBeenCalledTimes(350); + }); + + it('should initialize schema', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + extra: 'field', + }, + ]; + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + // Builder ref should be set to location (and only once). + expect(lunrBuilderRefSpy).toHaveBeenCalledTimes(1); + expect(lunrBuilderRefSpy).toHaveBeenLastCalledWith('location'); + + // Builder fields should be based on document fields. + expect(lunrBuilderFieldSpy).toHaveBeenCalledTimes(4); + expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('title'); + expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('text'); + expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('location'); + expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('extra'); + }); + + it('should configure lunr pipeline', async () => { + expect(lunrBuilderSearchPipelineAddSpy).toHaveBeenLastCalledWith( + lunr.stemmer, + ); + expect(lunrBuilderPipelineAddSpy).toHaveBeenCalledWith( + ...[lunr.trimmer, lunr.stopWordFilter, lunr.stemmer], + ); + }); +}); diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts new file mode 100644 index 0000000000..49afafea64 --- /dev/null +++ b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2022 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 { IndexableDocument } from '@backstage/plugin-search-common'; +import lunr from 'lunr'; +import { BatchSearchEngineIndexer } from '../indexing'; + +/** + * @beta + */ +export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer { + private schemaInitialized = false; + private builder: lunr.Builder; + private docStore: Record = {}; + + constructor() { + super({ batchSize: 100 }); + + this.builder = new lunr.Builder(); + this.builder.pipeline.add(lunr.trimmer, lunr.stopWordFilter, lunr.stemmer); + this.builder.searchPipeline.add(lunr.stemmer); + } + + // No async initialization required. + async initialize(): Promise {} + async finalize(): Promise {} + + async index(documents: IndexableDocument[]): Promise { + if (!this.schemaInitialized) { + // Make this lunr index aware of all relevant fields. + Object.keys(documents[0]).forEach(field => { + this.builder.field(field); + }); + + // Set "location" field as reference field + this.builder.ref('location'); + + this.schemaInitialized = true; + } + + documents.forEach(document => { + // Add document to Lunar index + this.builder.add(document); + + // Store documents in memory to be able to look up document using the ref during query time + // This is not how you should implement your SearchEngine implementation! Do not copy! + this.docStore[document.location] = document; + }); + } + + buildIndex() { + return this.builder.build(); + } + + getDocumentStore() { + return this.docStore; + } +} diff --git a/plugins/search-backend-node/src/engines/index.ts b/plugins/search-backend-node/src/engines/index.ts index 7e6fb86bd4..0d710eadc2 100644 --- a/plugins/search-backend-node/src/engines/index.ts +++ b/plugins/search-backend-node/src/engines/index.ts @@ -15,4 +15,8 @@ */ export { LunrSearchEngine } from './LunrSearchEngine'; -export type { ConcreteLunrQuery } from './LunrSearchEngine'; +export type { + ConcreteLunrQuery, + LunrQueryTranslator, +} from './LunrSearchEngine'; +export type { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; diff --git a/plugins/search-backend-node/src/index.ts b/plugins/search-backend-node/src/index.ts index d49ef195f2..e84c5d6a4d 100644 --- a/plugins/search-backend-node/src/index.ts +++ b/plugins/search-backend-node/src/index.ts @@ -23,8 +23,20 @@ export { IndexBuilder } from './IndexBuilder'; export { Scheduler } from './Scheduler'; export { LunrSearchEngine } from './engines'; +export type { + ConcreteLunrQuery, + LunrQueryTranslator, + LunrSearchEngineIndexer, +} from './engines'; +export type { + IndexBuilderOptions, + RegisterCollatorParameters, + RegisterDecoratorParameters, +} from './types'; +export * from './indexing'; +export * from './test-utils'; /** - * @deprecated Import from @backstage/search-common instead + * @deprecated Import from @backstage/plugin-search-common instead */ -export type { SearchEngine } from '@backstage/search-common'; +export type { SearchEngine } from '@backstage/plugin-search-common'; diff --git a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts new file mode 100644 index 0000000000..b4c9ff9e46 --- /dev/null +++ b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts @@ -0,0 +1,156 @@ +/* + * Copyright 2022 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 { IndexableDocument } from '@backstage/plugin-search-common'; +import { BatchSearchEngineIndexer } from './BatchSearchEngineIndexer'; +import { TestPipeline } from '../test-utils'; + +const indexSpy = jest.fn().mockResolvedValue(undefined); +const initializeSpy = jest.fn().mockResolvedValue(undefined); +const finalizeSpy = jest.fn().mockResolvedValue(undefined); + +class ConcreteBatchIndexer extends BatchSearchEngineIndexer { + async index(documents: IndexableDocument[]): Promise { + return indexSpy(documents); + } + async initialize(): Promise { + return initializeSpy(); + } + async finalize(): Promise { + return finalizeSpy(); + } +} + +describe('BatchSearchEngineIndexer', () => { + const document = { + title: 'Some Document', + text: 'Some document text.', + location: '/some/location', + }; + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should work end-to-end', async () => { + const indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + await TestPipeline.withSubject(indexer) + .withDocuments([document, document, document]) + .execute(); + expect(indexSpy).toHaveBeenCalledTimes(3); + }); + + it('should call initialize at construction', () => { + // @ts-expect-error + const _indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + + return new Promise(done => { + // Allow initialization to complete. + setImmediate(() => { + expect(initializeSpy).toHaveBeenCalled(); + done(); + }); + }); + }); + + it('should emit error if initialization throws', () => { + // Cause the initializer to throw. + const expectedError = new Error('some error'); + initializeSpy.mockRejectedValue(expectedError); + const indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + indexer.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + // Write a document to force the error state to become known. + indexer.write(document); + }); + }); + + it('should call index according to batchSize', () => { + const indexer = new ConcreteBatchIndexer({ batchSize: 2 }); + + return new Promise(done => { + // Listen for it to finish and assert the batches. + indexer.on('finish', () => { + expect(indexSpy).toHaveBeenCalledTimes(2); + expect(indexSpy).toHaveBeenNthCalledWith(1, [document, document]); + expect(indexSpy).toHaveBeenNthCalledWith(2, [document]); + done(); + }); + + // Write batchSize + 1 documents and end the stream. + indexer.write(document); + indexer.write(document); + indexer.write(document); + indexer.end(); + }); + }); + + it('should call index without exceeding batchSize', () => { + const indexer = new ConcreteBatchIndexer({ batchSize: 2 }); + + return new Promise(done => { + // Listen for it to finish and assert that it still wrote. + indexer.on('finish', () => { + expect(indexSpy).toHaveBeenCalledTimes(1); + expect(indexSpy).toHaveBeenNthCalledWith(1, [document]); + done(); + }); + + // Write batchSize - 1 documents and end the stream. + indexer.write(document); + indexer.end(); + }); + }); + + it('should emit error if index throws', () => { + // Cause the indexer to throw. + const expectedError = new Error('index error'); + indexSpy.mockRejectedValue(expectedError); + const indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + indexer.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + indexer.write(document); + }); + }); + + it('should emit error if finalize throws', () => { + // Cause the indexer to throw. + const expectedError = new Error('finalize error'); + finalizeSpy.mockRejectedValue(expectedError); + const indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + indexer.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + indexer.end(); + }); + }); +}); diff --git a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts new file mode 100644 index 0000000000..6070a577ed --- /dev/null +++ b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts @@ -0,0 +1,125 @@ +/* + * Copyright 2022 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 { assertError } from '@backstage/errors'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { Writable } from 'stream'; + +/** + * @beta + */ +export type BatchSearchEngineOptions = { + batchSize: number; +}; + +/** + * Base class encapsulating batch-based stream processing. Useful as a base + * class for search engine indexers. + * @beta + */ +export abstract class BatchSearchEngineIndexer extends Writable { + private batchSize: number; + private currentBatch: IndexableDocument[] = []; + private initialized: Promise; + + constructor(options: BatchSearchEngineOptions) { + super({ objectMode: true }); + this.batchSize = options.batchSize; + + // @todo Once node v15 is minimum, convert to _construct implementation. + this.initialized = new Promise(done => { + // Necessary to allow concrete implementation classes to construct + // themselves before calling their initialize() methods. + setImmediate(async () => { + try { + await this.initialize(); + done(undefined); + } catch (e) { + assertError(e); + done(e); + } + }); + }); + } + + /** + * Receives an array of indexable documents (of size this.batchSize) which + * should be written to the search engine. This method won't be called again + * at least until it resolves. + */ + public abstract index(documents: IndexableDocument[]): Promise; + + /** + * Any asynchronous setup tasks can be performed here. + */ + public abstract initialize(): Promise; + + /** + * Any asynchronous teardown tasks can be performed here. + */ + public abstract finalize(): Promise; + + /** + * Encapsulates batch stream write logic. + * @internal + */ + async _write( + doc: IndexableDocument, + _e: any, + done: (error?: Error | null) => void, + ) { + // Wait for init before proceeding. Throw error if initialization failed. + const maybeError = await this.initialized; + if (maybeError) { + done(maybeError); + return; + } + + this.currentBatch.push(doc); + if (this.currentBatch.length < this.batchSize) { + done(); + return; + } + + try { + await this.index(this.currentBatch); + this.currentBatch = []; + done(); + } catch (e) { + assertError(e); + done(e); + } + } + + /** + * Encapsulates finalization and final error handling logic. + * @internal + */ + async _final(done: (error?: Error | null) => void) { + try { + // Index any remaining documents. + if (this.currentBatch.length) { + await this.index(this.currentBatch); + this.currentBatch = []; + } + await this.finalize(); + done(); + } catch (e) { + assertError(e); + done(e); + } + } +} diff --git a/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts b/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts new file mode 100644 index 0000000000..12c61bd7d3 --- /dev/null +++ b/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts @@ -0,0 +1,157 @@ +/* + * Copyright 2022 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 { IndexableDocument } from '@backstage/plugin-search-common'; +import { DecoratorBase } from './DecoratorBase'; +import { TestPipeline } from '../test-utils'; + +const decorateSpy = jest.fn().mockResolvedValue(undefined); +const initializeSpy = jest.fn().mockResolvedValue(undefined); +const finalizeSpy = jest.fn().mockResolvedValue(undefined); + +class ConcreteDecorator extends DecoratorBase { + public initialize(): Promise { + return initializeSpy(); + } + public decorate( + document: IndexableDocument, + ): Promise { + return decorateSpy(document); + } + public finalize(): Promise { + return finalizeSpy(); + } +} + +describe('DecoratorBase', () => { + const document = { + title: 'Some Document', + text: 'Some document text.', + location: '/some/location', + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should work end-to-end', async () => { + decorateSpy.mockImplementation(doc => ({ + ...doc, + transformed: true, + })); + + const decorator = new ConcreteDecorator(); + const { documents } = await TestPipeline.withSubject(decorator) + .withDocuments([document, document, document]) + .execute(); + + expect(documents.length).toBe(3); + expect((documents[0] as unknown as any).transformed).toBe(true); + expect((documents[1] as unknown as any).transformed).toBe(true); + expect((documents[2] as unknown as any).transformed).toBe(true); + }); + + it('should allow filtering', async () => { + decorateSpy.mockResolvedValue(undefined); + + const decorator = new ConcreteDecorator(); + const { documents } = await TestPipeline.withSubject(decorator) + .withDocuments([document, document, document]) + .execute(); + + expect(decorateSpy).toHaveBeenCalledTimes(3); + expect(documents.length).toBe(0); + }); + + it('should allow fanning', async () => { + decorateSpy.mockImplementation(doc => { + return [doc, doc]; + }); + + const decorator = new ConcreteDecorator(); + const { documents } = await TestPipeline.withSubject(decorator) + .withDocuments([document, document, document]) + .execute(); + + expect(decorateSpy).toHaveBeenCalledTimes(3); + expect(documents.length).toBe(6); + }); + + it('should call initialize at construction', () => { + // @ts-expect-error + const _indexer = new ConcreteDecorator(); + + return new Promise(done => { + // Allow initialization to complete. + setImmediate(() => { + expect(initializeSpy).toHaveBeenCalled(); + done(); + }); + }); + }); + + it('should emit error if initialization throws', () => { + // Cause the initializer to throw. + const expectedError = new Error('some error'); + initializeSpy.mockRejectedValue(expectedError); + const decorator = new ConcreteDecorator(); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + decorator.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + // Write a document to force the error state to become known. + decorator.write(document); + }); + }); + + it('should emit error if index throws', () => { + // Cause the indexer to throw. + const expectedError = new Error('decorate error'); + decorateSpy.mockRejectedValue(expectedError); + const decorator = new ConcreteDecorator(); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + decorator.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + decorator.write(document); + }); + }); + + it('should emit error if finalize throws', () => { + // Cause the indexer to throw. + const expectedError = new Error('finalize error'); + finalizeSpy.mockRejectedValue(expectedError); + const decorator = new ConcreteDecorator(); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + decorator.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + decorator.end(); + }); + }); +}); diff --git a/plugins/search-backend-node/src/indexing/DecoratorBase.ts b/plugins/search-backend-node/src/indexing/DecoratorBase.ts new file mode 100644 index 0000000000..0541ab82d7 --- /dev/null +++ b/plugins/search-backend-node/src/indexing/DecoratorBase.ts @@ -0,0 +1,127 @@ +/* + * Copyright 2022 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 { assertError } from '@backstage/errors'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { Transform } from 'stream'; + +/** + * Base class encapsulating simple async transformations. Useful as a base + * class for Backstage search decorators. + * @beta + */ +export abstract class DecoratorBase extends Transform { + private initialized: Promise; + + constructor() { + super({ objectMode: true }); + + // @todo Once node v15 is minimum, convert to _construct implementation. + this.initialized = new Promise(done => { + // Necessary to allow concrete implementation classes to construct + // themselves before calling their initialize() methods. + setImmediate(async () => { + try { + await this.initialize(); + done(undefined); + } catch (e) { + assertError(e); + done(e); + } + }); + }); + } + + /** + * Any asynchronous setup tasks can be performed here. + */ + public abstract initialize(): Promise; + + /** + * Receives a single indexable document. In your decorate method, you can: + * + * - Resolve `undefined` to indicate the record should be omitted. + * - Resolve a single modified document, which could contain new fields, + * edited fields, or removed fields. + * - Resolve an array of indexable documents, if the purpose if the decorator + * is to convert one document into multiple derivative documents. + */ + public abstract decorate( + document: IndexableDocument, + ): Promise; + + /** + * Any asynchronous teardown tasks can be performed here. + */ + public abstract finalize(): Promise; + + /** + * Encapsulates simple transform stream logic. + * @internal + */ + async _transform( + document: IndexableDocument, + _: any, + done: (error?: Error | null) => void, + ) { + // Wait for init before proceeding. Throw error if initialization failed. + const maybeError = await this.initialized; + if (maybeError) { + done(maybeError); + return; + } + + try { + const decorated = await this.decorate(document); + + // If undefined was returned, omit the record and move on. + if (decorated === undefined) { + done(); + return; + } + + // If an array of documents was given, push them all. + if (Array.isArray(decorated)) { + decorated.forEach(doc => { + this.push(doc); + }); + done(); + return; + } + + // Otherwise, just push the decorated document. + this.push(decorated); + done(); + } catch (e) { + assertError(e); + done(e); + } + } + + /** + * Encapsulates finalization and final error handling logic. + * @internal + */ + async _final(done: (error?: Error | null) => void) { + try { + await this.finalize(); + done(); + } catch (e) { + assertError(e); + done(e); + } + } +} diff --git a/plugins/search-backend-node/src/indexing/index.ts b/plugins/search-backend-node/src/indexing/index.ts new file mode 100644 index 0000000000..e86235df16 --- /dev/null +++ b/plugins/search-backend-node/src/indexing/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 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. + */ + +export { BatchSearchEngineIndexer } from './BatchSearchEngineIndexer'; +export { DecoratorBase } from './DecoratorBase'; +export type { BatchSearchEngineOptions } from './BatchSearchEngineIndexer'; diff --git a/plugins/search-backend-node/src/test-utils/TestPipeline.ts b/plugins/search-backend-node/src/test-utils/TestPipeline.ts new file mode 100644 index 0000000000..162572d929 --- /dev/null +++ b/plugins/search-backend-node/src/test-utils/TestPipeline.ts @@ -0,0 +1,140 @@ +/* + * Copyright 2022 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 { IndexableDocument } from '@backstage/plugin-search-common'; +import { pipeline, Readable, Transform, Writable } from 'stream'; + +/** + * Object resolved after a test pipeline is executed. + * @beta + */ +export type TestPipelineResult = { + /** + * If an error was emitted by the pipeline, it will be set here. + */ + error: unknown; + + /** + * A list of documents collected at the end of the pipeline. If the subject + * under test is an indexer, this will be an empty array (because your + * indexer should have received the documents instead). + */ + documents: IndexableDocument[]; +}; + +/** + * Test utility for Backstage Search collators, decorators, and indexers. + * @beta + */ +export class TestPipeline { + private collator?: Readable; + private decorator?: Transform; + private indexer?: Writable; + + private constructor({ + collator, + decorator, + indexer, + }: { + collator?: Readable; + decorator?: Transform; + indexer?: Writable; + }) { + this.collator = collator; + this.decorator = decorator; + this.indexer = indexer; + } + + /** + * Provide the collator, decorator, or indexer to be tested. + */ + static withSubject(subject: Readable | Transform | Writable) { + if (subject instanceof Transform) { + return new TestPipeline({ decorator: subject }); + } + + if (subject instanceof Readable) { + return new TestPipeline({ collator: subject }); + } + + if (subject instanceof Writable) { + return new TestPipeline({ indexer: subject }); + } + + throw new Error( + 'Unknown test subject: are you passing a readable, writable, or transform stream?', + ); + } + + /** + * Provide documents for testing decorators and indexers. + */ + withDocuments(documents: IndexableDocument[]): TestPipeline { + if (this.collator) { + throw new Error('Cannot provide documents when testing a collator.'); + } + + // Set a naive readable stream that just pushes all given documents. + this.collator = new Readable({ objectMode: true }); + this.collator._read = () => {}; + process.nextTick(() => { + documents.forEach(document => { + this.collator!.push(document); + }); + this.collator!.push(null); + }); + + return this; + } + + /** + * Execute the test pipeline so that you can make assertions about the result + * or behavior of the given test subject. + */ + async execute(): Promise { + const documents: IndexableDocument[] = []; + if (!this.collator) { + throw new Error( + 'Cannot execute pipeline without a collator or documents', + ); + } + + // If we are here and there is no indexer, we are testing a collator or a + // decorator. Set up a naive writable that captures documents in memory. + if (!this.indexer) { + this.indexer = new Writable({ objectMode: true }); + this.indexer._write = (document: IndexableDocument, _, done) => { + documents.push(document); + done(); + }; + } + + return new Promise(done => { + const pipes: (Readable | Transform | Writable)[] = [this.collator!]; + if (this.decorator) { + pipes.push(this.decorator); + } + pipes.push(this.indexer!); + + pipeline(pipes, error => { + done({ + error, + documents, + }); + }); + }); + } +} diff --git a/plugins/search-backend-node/src/test-utils/index.ts b/plugins/search-backend-node/src/test-utils/index.ts new file mode 100644 index 0000000000..185d25433b --- /dev/null +++ b/plugins/search-backend-node/src/test-utils/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 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. + */ + +export { TestPipeline } from './TestPipeline'; +export type { TestPipelineResult } from './TestPipeline'; diff --git a/plugins/search-backend-node/src/types.ts b/plugins/search-backend-node/src/types.ts index df83357f6c..d21bfaf453 100644 --- a/plugins/search-backend-node/src/types.ts +++ b/plugins/search-backend-node/src/types.ts @@ -14,10 +14,24 @@ * limitations under the License. */ -import { DocumentCollator, DocumentDecorator } from '@backstage/search-common'; +import { + DocumentCollatorFactory, + DocumentDecoratorFactory, + SearchEngine, +} from '@backstage/plugin-search-common'; +import { Logger } from 'winston'; + +/** + * @beta + */ +export type IndexBuilderOptions = { + searchEngine: SearchEngine; + logger: Logger; +}; /** * Parameters required to register a collator. + * @beta */ export interface RegisterCollatorParameters { /** @@ -26,17 +40,18 @@ export interface RegisterCollatorParameters { defaultRefreshIntervalSeconds: number; /** - * The collator class responsible for returning all documents of the given type. + * The class responsible for returning the document collator of the given type. */ - collator: DocumentCollator; + factory: DocumentCollatorFactory; } /** * Parameters required to register a decorator + * @beta */ export interface RegisterDecoratorParameters { /** - * The decorator class responsible for appending or modifying documents of the given type(s). + * The class responsible for returning the decorator which appends, modifies, or filters documents. */ - decorator: DocumentDecorator; + factory: DocumentDecoratorFactory; } diff --git a/plugins/search-backend/.eslintrc.js b/plugins/search-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/search-backend/.eslintrc.js +++ b/plugins/search-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index a802848287..e3383f5b02 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-search-backend +## 0.4.7-next.0 + +### Patch Changes + +- 3e54f6c436: Use `@backstage/plugin-search-common` package instead of `@backstage/search-common`. +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-search-common@0.3.1-next.0 + - @backstage/plugin-search-backend-node@0.5.1-next.0 + - @backstage/plugin-auth-node@0.1.5-next.0 + - @backstage/plugin-permission-node@0.5.4-next.0 + +## 0.4.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-permission-node@0.5.3 + - @backstage/plugin-search-backend-node@0.5.0 + - @backstage/search-common@0.3.0 + - @backstage/plugin-auth-node@0.1.4 + ## 0.4.5 ### Patch Changes diff --git a/plugins/search-backend/api-report.md b/plugins/search-backend/api-report.md index 796f54c09d..3c7f9c2754 100644 --- a/plugins/search-backend/api-report.md +++ b/plugins/search-backend/api-report.md @@ -4,9 +4,9 @@ ```ts import { Config } from '@backstage/config'; -import { DocumentTypeInfo } from '@backstage/search-common'; +import { DocumentTypeInfo } from '@backstage/plugin-search-common'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; @@ -23,6 +23,6 @@ export type RouterOptions = { types: Record; permissions: PermissionAuthorizer; config: Config; - logger: Logger_2; + logger: Logger; }; ``` diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index cdfb1afead..1af459a5a4 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend", "description": "The Backstage backend plugin that provides your backstage app with search", - "version": "0.4.5", + "version": "0.4.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,14 +23,14 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/search-common": "^0.2.4", - "@backstage/plugin-auth-node": "^0.1.3", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-permission-node": "^0.5.2", - "@backstage/plugin-search-backend-node": "^0.4.7", + "@backstage/plugin-auth-node": "^0.1.5-next.0", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-permission-node": "^0.5.4-next.0", + "@backstage/plugin-search-backend-node": "^0.5.1-next.0", + "@backstage/plugin-search-common": "^0.3.1-next.0", "@backstage/types": "^0.1.3", "@types/express": "^4.17.6", "dataloader": "^2.0.0", @@ -43,7 +43,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts index 0a7b45e164..ba63f8484e 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts @@ -24,7 +24,7 @@ import { DocumentTypeInfo, IndexableDocument, SearchEngine, -} from '@backstage/search-common'; +} from '@backstage/plugin-search-common'; import { encodePageCursor, decodePageCursor, @@ -62,7 +62,7 @@ describe('AuthorizedSearchEngine', () => { setTranslator: () => { throw new Error('Function not implemented. 1'); }, - index: () => { + getIndexer: () => { throw new Error('Function not implemented.2'); }, query: mockedQuery, diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts index d0488af00d..2e65e18a84 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts @@ -25,16 +25,16 @@ import { } from '@backstage/plugin-permission-common'; import { DocumentTypeInfo, - IndexableDocument, QueryRequestOptions, QueryTranslator, SearchEngine, SearchQuery, SearchResult, SearchResultSet, -} from '@backstage/search-common'; +} from '@backstage/plugin-search-common'; import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; +import { Writable } from 'stream'; export function decodePageCursor(pageCursor?: string): { page: number } { if (!pageCursor) { @@ -78,8 +78,8 @@ export class AuthorizedSearchEngine implements SearchEngine { this.searchEngine.setTranslator(translator); } - async index(type: string, documents: IndexableDocument[]): Promise { - this.searchEngine.index(type, documents); + async getIndexer(type: string): Promise { + return this.searchEngine.getIndexer(type); } async query( diff --git a/plugins/search-backend/src/service/router.test.ts b/plugins/search-backend/src/service/router.test.ts index 0a94250aeb..bdf46a240b 100644 --- a/plugins/search-backend/src/service/router.test.ts +++ b/plugins/search-backend/src/service/router.test.ts @@ -105,7 +105,7 @@ describe('createRouter', () => { beforeAll(async () => { const logger = getVoidLogger(); mockSearchEngine = { - index: jest.fn(), + getIndexer: jest.fn(), setTranslator: jest.fn(), query: jest.fn(), }; diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 3f25c4f24a..92e4e5526f 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -24,7 +24,10 @@ import { Config } from '@backstage/config'; import { JsonObject, JsonValue } from '@backstage/types'; import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; -import { DocumentTypeInfo, SearchResultSet } from '@backstage/search-common'; +import { + DocumentTypeInfo, + SearchResultSet, +} from '@backstage/plugin-search-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; import { AuthorizedSearchEngine } from './AuthorizedSearchEngine'; diff --git a/plugins/search-common/.eslintrc.js b/plugins/search-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/search-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search-common/CHANGELOG.md b/plugins/search-common/CHANGELOG.md new file mode 100644 index 0000000000..5d292b3712 --- /dev/null +++ b/plugins/search-common/CHANGELOG.md @@ -0,0 +1,154 @@ +# @backstage/plugin-search-common + +## 0.3.1-next.0 + +### Patch Changes + +- d52155466a: Renamed `@backstage/search-common` to `@backstage/plugin-search-common`. + +## 0.3.0 + +### Minor Changes + +- 022507c860: **BREAKING** + + The Backstage Search Platform's indexing process has been rewritten as a stream + pipeline in order to improve efficiency and performance on large document sets. + + The concepts of `Collator` and `Decorator` have been replaced with readable and + transform object streams (respectively), as well as factory classes to + instantiate them. Accordingly, the `SearchEngine.index()` method has also been + replaced with a `getIndexer()` factory method that resolves to a writable + object stream. + + Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for further details. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.5.2 + +## 0.2.4 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/types@0.1.3 + - @backstage/plugin-permission-common@0.5.1 + +## 0.2.3 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/plugin-permission-common@0.5.0 + - @backstage/types@0.1.2 + +## 0.2.2 + +### Patch Changes + +- 9a511968b1: - Add optional visibilityPermission property to DocumentCollator type + - Add new DocumentTypeInfo type for housing information about the document types stored in a search engine. +- b2e918fa0b: Add optional resourceRef field to the IndexableDocument type for use when authorizing access to documents. +- 96cbebc629: Add optional query request options containing authorization token to SearchEngine#query. + +## 0.2.1 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` + +## 0.2.0 + +### Minor Changes + +- a13f21cdc: Implement optional `pageCursor` based paging in search. + + To use paging in your app, add a `` to your + `SearchPage.tsx`. + +## 0.1.3 + +### Patch Changes + +- d9c13d535: Implements configuration and indexing functionality for ElasticSearch search engine. Adds indexing, searching and default translator for ElasticSearch and modifies default backend example-app to use ES if it is configured. + + ## Example configurations: + + ### AWS + + Using AWS hosted ElasticSearch the only configuration options needed is the URL to the ElasticSearch service. The implementation assumes + that environment variables for AWS access key id and secret access key are defined in accordance to the [default AWS credential chain.](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html). + + ```yaml + search: + elasticsearch: + provider: aws + node: https://my-backstage-search-asdfqwerty.eu-west-1.es.amazonaws.com + ``` + + ### Elastic.co + + Elastic Cloud hosted ElasticSearch uses a Cloud ID to determine the instance of hosted ElasticSearch to connect to. Additionally, username and password needs to be provided either directly or using environment variables like defined in [Backstage documentation.](https://backstage.io/docs/conf/writing#includes-and-dynamic-data) + + ```yaml + search: + elasticsearch: + provider: elastic + cloudId: backstage-elastic:asdfqwertyasdfqwertyasdfqwertyasdfqwerty== + auth: + username: elastic + password: changeme + ``` + + ### Others + + Other ElasticSearch instances can be connected to by using standard ElasticSearch authentication methods and exposed URL, provided that the cluster supports that. The configuration options needed are the URL to the node and authentication information. Authentication can be handled by either providing username/password or and API key or a bearer token. In case both username/password combination and one of the tokens are provided, token takes precedence. For more information how to create an API key, see [Elastic documentation on API keys](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html) and how to create a bearer token, see [Elastic documentation on tokens.](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-service-token.html) + + #### Configuration examples + + ##### With username and password + + ```yaml + search: + elasticsearch: + node: http://localhost:9200 + auth: + username: elastic + password: changeme + ``` + + ##### With bearer token + + ```yaml + search: + elasticsearch: + node: http://localhost:9200 + auth: + bearer: token + ``` + + ##### With API key + + ```yaml + search: + elasticsearch: + node: http://localhost:9200 + auth: + apiKey: base64EncodedKey + ``` + +- Updated dependencies + - @backstage/config@0.1.6 + +## 0.1.2 + +### Patch Changes + +- db1c8f93b: The ` set of components exported by the Search Plugin are now updated to use the Search Backend API. These will be made available as the default non-"next" versions in a follow-up release. + + The interfaces for decorators and collators in the Search Backend have also seen minor, breaking revisions ahead of a general release. If you happen to be building on top of these interfaces, check and update your implementations accordingly. The APIs will be considered more stable in a follow-up release. diff --git a/plugins/search-common/README.md b/plugins/search-common/README.md new file mode 100644 index 0000000000..7299c758d4 --- /dev/null +++ b/plugins/search-common/README.md @@ -0,0 +1,3 @@ +# @backstage/plugin-search-common + +Common functionalities for Search, to be shared between various search-enabled plugins. diff --git a/plugins/search-common/api-report.md b/plugins/search-common/api-report.md new file mode 100644 index 0000000000..8ab25d5e42 --- /dev/null +++ b/plugins/search-common/api-report.md @@ -0,0 +1,89 @@ +## API Report File for "@backstage/plugin-search-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { JsonObject } from '@backstage/types'; +import { Permission } from '@backstage/plugin-permission-common'; +import { Readable } from 'stream'; +import { Transform } from 'stream'; +import { Writable } from 'stream'; + +// @beta +export interface DocumentCollatorFactory { + getCollator(): Promise; + readonly type: string; + readonly visibilityPermission?: Permission; +} + +// @beta +export interface DocumentDecoratorFactory { + getDecorator(): Promise; + readonly types?: string[]; +} + +// @beta +export type DocumentTypeInfo = { + visibilityPermission?: Permission; +}; + +// @beta +export interface IndexableDocument { + authorization?: { + resourceRef: string; + }; + location: string; + text: string; + title: string; +} + +// @beta +export type QueryRequestOptions = { + token?: string; +}; + +// @beta +export type QueryTranslator = (query: SearchQuery) => unknown; + +// @beta +export interface SearchEngine { + getIndexer(type: string): Promise; + query( + query: SearchQuery, + options?: QueryRequestOptions, + ): Promise; + setTranslator(translator: QueryTranslator): void; +} + +// @beta (undocumented) +export interface SearchQuery { + // (undocumented) + filters?: JsonObject; + // (undocumented) + pageCursor?: string; + // (undocumented) + term: string; + // (undocumented) + types?: string[]; +} + +// @beta (undocumented) +export interface SearchResult { + // (undocumented) + document: IndexableDocument; + // (undocumented) + type: string; +} + +// @beta (undocumented) +export interface SearchResultSet { + // (undocumented) + nextPageCursor?: string; + // (undocumented) + previousPageCursor?: string; + // (undocumented) + results: SearchResult[]; +} +``` diff --git a/plugins/search-common/package.json b/plugins/search-common/package.json new file mode 100644 index 0000000000..bb2564d713 --- /dev/null +++ b/plugins/search-common/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-search-common", + "description": "Common functionalities for Search, to be shared between various search-enabled plugins", + "version": "0.3.1-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/search-common" + }, + "keywords": [ + "backstage", + "search" + ], + "license": "Apache-2.0", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" + }, + "bugs": { + "url": "https://github.com/backstage/backstage/issues" + }, + "dependencies": { + "@backstage/types": "^0.1.3", + "@backstage/plugin-permission-common": "^0.5.2" + }, + "devDependencies": { + "@backstage/cli": "^0.15.2-next.0" + }, + "jest": { + "roots": [ + ".." + ] + } +} diff --git a/packages/search-common/src/index.test.ts b/plugins/search-common/src/index.test.ts similarity index 100% rename from packages/search-common/src/index.test.ts rename to plugins/search-common/src/index.test.ts diff --git a/plugins/search-common/src/index.ts b/plugins/search-common/src/index.ts new file mode 100644 index 0000000000..e71f8cd660 --- /dev/null +++ b/plugins/search-common/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Common functionalities for Search, to be shared between various search-enabled plugins + * + * @packageDocumentation + */ + +export * from './types'; diff --git a/packages/search-common/src/types.ts b/plugins/search-common/src/types.ts similarity index 77% rename from packages/search-common/src/types.ts rename to plugins/search-common/src/types.ts index 4e61767ea0..51ce45617b 100644 --- a/packages/search-common/src/types.ts +++ b/plugins/search-common/src/types.ts @@ -16,7 +16,11 @@ import { Permission } from '@backstage/plugin-permission-common'; import { JsonObject } from '@backstage/types'; +import { Readable, Transform, Writable } from 'stream'; +/** + * @beta + */ export interface SearchQuery { term: string; filters?: JsonObject; @@ -24,11 +28,17 @@ export interface SearchQuery { pageCursor?: string; } +/** + * @beta + */ export interface SearchResult { type: string; document: IndexableDocument; } +/** + * @beta + */ export interface SearchResultSet { results: SearchResult[]; nextPageCursor?: string; @@ -38,6 +48,7 @@ export interface SearchResultSet { /** * Base properties that all indexed documents must include, as well as some * common properties that documents are encouraged to use where appropriate. + * @beta */ export interface IndexableDocument { /** @@ -72,6 +83,7 @@ export interface IndexableDocument { * Information about a specific document type. Intended to be used in the * {@link @backstage/search-backend-node#IndexBuilder} to collect information * about the types stored in the index. + * @beta */ export type DocumentTypeInfo = { /** @@ -82,10 +94,10 @@ export type DocumentTypeInfo = { }; /** - * Interface that must be implemented in order to expose new documents to - * search. + * Factory class for instantiating collators. + * @beta */ -export interface DocumentCollator { +export interface DocumentCollatorFactory { /** * The type or name of the document set returned by this collator. Used as an * index name by Search Engines. @@ -98,29 +110,41 @@ export interface DocumentCollator { */ readonly visibilityPermission?: Permission; - execute(): Promise; + /** + * Instantiates and resolves a document collator. + */ + getCollator(): Promise; } /** - * Interface that must be implemented in order to decorate existing documents with - * additional metadata. + * Factory class for instantiating decorators. + * @beta */ -export interface DocumentDecorator { +export interface DocumentDecoratorFactory { /** * An optional array of document/index types on which this decorator should * be applied. If no types are provided, this decorator will be applied to * all document/index types. */ readonly types?: string[]; - execute(documents: IndexableDocument[]): Promise; + + /** + * Instantiates and resolves a document decorator. + */ + getDecorator(): Promise; } /** * A type of function responsible for translating an abstract search query into * a concrete query relevant to a particular search engine. + * @beta */ export type QueryTranslator = (query: SearchQuery) => unknown; +/** + * Options when querying a search engine. + * @beta + */ export type QueryRequestOptions = { token?: string; }; @@ -129,6 +153,7 @@ export type QueryRequestOptions = { * Interface that must be implemented by specific search engines, responsible * for performing indexing and querying and translating abstract queries into * concrete, search engine-specific queries. + * @beta */ export interface SearchEngine { /** @@ -137,9 +162,15 @@ export interface SearchEngine { setTranslator(translator: QueryTranslator): void; /** - * Add the given documents to the SearchEngine index of the given type. + * Factory method for getting a search engine indexer for a given document + * type. + * + * @param type - The type or name of the document set for which an indexer + * should be retrieved. This corresponds to the `type` property on the + * document collator/decorator factories and will most often be used to + * identify an index or group to which documents should be written. */ - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; /** * Perform a search query against the SearchEngine. diff --git a/plugins/search/.eslintrc.js b/plugins/search/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/search/.eslintrc.js +++ b/plugins/search/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index a0e80eaeee..c6020e7feb 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-search +## 0.7.3-next.0 + +### Patch Changes + +- 3e54f6c436: Use `@backstage/plugin-search-common` package instead of `@backstage/search-common`. +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/plugin-search-common@0.3.1-next.0 + +## 0.7.2 + +### Patch Changes + +- 64b430f80d: chore(deps): bump `react-text-truncate` from 0.17.0 to 0.18.0 +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/search-common@0.3.0 + ## 0.7.1 ### Patch Changes diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 10ce787a6c..0a277dab85 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -9,16 +9,16 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { AsyncState } from 'react-use/lib/useAsync'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { InputBaseProps } from '@material-ui/core'; import { JsonObject } from '@backstage/types'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; -import { SearchQuery } from '@backstage/search-common'; -import { SearchResult as SearchResult_2 } from '@backstage/search-common'; -import { SearchResultSet } from '@backstage/search-common'; +import { SearchQuery } from '@backstage/plugin-search-common'; +import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common'; +import { SearchResultSet } from '@backstage/plugin-search-common'; // Warning: (ae-missing-release-tag) "DefaultResultListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/search/package.json b/plugins/search/package.json index 4543aa8dcd..4808ce7aa8 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search", "description": "The Backstage plugin that provides your backstage app with search", - "version": "0.7.1", + "version": "0.7.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,13 +33,13 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/search-common": "^0.2.4", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", + "@backstage/plugin-search-common": "^0.3.1-next.0", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", @@ -56,10 +56,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/search/src/apis.ts b/plugins/search/src/apis.ts index 008002deb1..908942d87d 100644 --- a/plugins/search/src/apis.ts +++ b/plugins/search/src/apis.ts @@ -20,7 +20,7 @@ import { IdentityApi, } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; -import { SearchQuery, SearchResultSet } from '@backstage/search-common'; +import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common'; import qs from 'qs'; export const searchApiRef = createApiRef({ diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx index a6c9a36bb6..d9fbfe315e 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx +++ b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx @@ -15,7 +15,7 @@ */ import React, { ReactNode } from 'react'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { ListItem, ListItemIcon, diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx index a9f42fe5ca..100d8a3ac3 100644 --- a/plugins/search/src/components/SearchContext/SearchContext.tsx +++ b/plugins/search/src/components/SearchContext/SearchContext.tsx @@ -16,7 +16,7 @@ import { JsonObject } from '@backstage/types'; import { useApi, AnalyticsContext } from '@backstage/core-plugin-api'; -import { SearchResultSet } from '@backstage/search-common'; +import { SearchResultSet } from '@backstage/plugin-search-common'; import React, { createContext, PropsWithChildren, diff --git a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx index 36d670a1ed..7d6c35b00c 100644 --- a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx +++ b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { ApiProvider } from '@backstage/core-app-api'; -import { SearchResultSet } from '@backstage/search-common'; +import { SearchResultSet } from '@backstage/plugin-search-common'; import { TestApiRegistry } from '@backstage/test-utils'; import React, { ComponentProps, PropsWithChildren } from 'react'; import { searchApiRef } from '../../apis'; diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index 34b6840f21..d6ca9fd968 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -19,7 +19,7 @@ import { Progress, ResponseErrorPanel, } from '@backstage/core-components'; -import { SearchResult } from '@backstage/search-common'; +import { SearchResult } from '@backstage/plugin-search-common'; import React from 'react'; import { useSearch } from '../SearchContext'; diff --git a/plugins/sentry/.eslintrc.js b/plugins/sentry/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/sentry/.eslintrc.js +++ b/plugins/sentry/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 44f20e05d1..5a0d031a61 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-sentry +## 0.3.40-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.3.39 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.38 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 1d6db3a846..50c3f2d55a 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sentry", "description": "A Backstage plugin that integrates towards Sentry", - "version": "0.3.38", + "version": "0.3.40-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,10 +35,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -52,10 +52,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/shortcuts/.eslintrc.js b/plugins/shortcuts/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/shortcuts/.eslintrc.js +++ b/plugins/shortcuts/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index f60d458dd0..ca9b9e33b2 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-shortcuts +## 0.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.1-next.0 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.2.1 ### Patch Changes diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 2407f4b84b..fa3bd918dd 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-shortcuts", "description": "A Backstage plugin that provides a shortcuts feature to the sidebar", - "version": "0.2.1", + "version": "0.2.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,8 +24,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", @@ -42,10 +42,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/sonarqube/.eslintrc.js b/plugins/sonarqube/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/sonarqube/.eslintrc.js +++ b/plugins/sonarqube/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index 53209faa93..0ecc3b4a57 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-sonarqube +## 0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.0 ### Minor Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 38aada552f..04dc5a1580 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sonarqube", "description": "", - "version": "0.3.0", + "version": "0.3.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,10 +36,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -53,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/splunk-on-call/.eslintrc.js b/plugins/splunk-on-call/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/splunk-on-call/.eslintrc.js +++ b/plugins/splunk-on-call/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index 8667f46749..eeb4b613f9 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-splunk-on-call +## 0.3.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.3.25 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.24 ### Patch Changes diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 7d8f6b5bdf..15ef124c65 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-splunk-on-call", "description": "A Backstage plugin that integrates towards Splunk On-Call", - "version": "0.3.24", + "version": "0.3.26-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,10 +34,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -51,10 +51,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js b/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js +++ b/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md index b34cb336cd..1541a31e59 100644 --- a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md +++ b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-tech-insights-backend-module-jsonfc +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-tech-insights-node@0.2.7-next.0 + +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-tech-insights-node@0.2.6 + ## 0.1.11 ### Patch Changes diff --git a/plugins/tech-insights-backend-module-jsonfc/api-report.md b/plugins/tech-insights-backend-module-jsonfc/api-report.md index 2e62e7d6a0..69e4054170 100644 --- a/plugins/tech-insights-backend-module-jsonfc/api-report.md +++ b/plugins/tech-insights-backend-module-jsonfc/api-report.md @@ -7,7 +7,7 @@ import { BooleanCheckResult } from '@backstage/plugin-tech-insights-common'; import { CheckResponse } from '@backstage/plugin-tech-insights-common'; import { CheckValidationResponse } from '@backstage/plugin-tech-insights-node'; import { FactChecker } from '@backstage/plugin-tech-insights-node'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { Operator } from 'json-rules-engine'; import { TechInsightCheck } from '@backstage/plugin-tech-insights-node'; import { TechInsightCheckRegistry } from '@backstage/plugin-tech-insights-node'; @@ -79,7 +79,7 @@ export class JsonRulesEngineFactCheckerFactory { // @public export type JsonRulesEngineFactCheckerFactoryOptions = { checks: TechInsightJsonRuleCheck[]; - logger: Logger_2; + logger: Logger; checkRegistry?: TechInsightCheckRegistry; operators?: Operator[]; }; @@ -88,7 +88,7 @@ export type JsonRulesEngineFactCheckerFactoryOptions = { export type JsonRulesEngineFactCheckerOptions = { checks: TechInsightJsonRuleCheck[]; repository: TechInsightsStore; - logger: Logger_2; + logger: Logger; checkRegistry?: TechInsightCheckRegistry; operators?: Operator[]; }; diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index 5c618d4172..36e39ea3a3 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend-module-jsonfc", - "version": "0.1.11", + "version": "0.1.13-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,11 +34,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@backstage/plugin-tech-insights-common": "^0.2.3", - "@backstage/plugin-tech-insights-node": "^0.2.5", + "@backstage/plugin-tech-insights-node": "^0.2.7-next.0", "ajv": "^7.0.3", "json-rules-engine": "^6.1.2", "lodash": "^4.17.21", @@ -46,7 +46,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/node-cron": "^3.0.1" }, "files": [ diff --git a/plugins/tech-insights-backend/.eslintrc.js b/plugins/tech-insights-backend/.eslintrc.js index a126c438a4..f98046cfe0 100644 --- a/plugins/tech-insights-backend/.eslintrc.js +++ b/plugins/tech-insights-backend/.eslintrc.js @@ -1,5 +1,4 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { 'jest/expect-expect': [ 'error', @@ -8,4 +7,4 @@ module.exports = { }, ], }, -}; +}); diff --git a/plugins/tech-insights-backend/CHANGELOG.md b/plugins/tech-insights-backend/CHANGELOG.md index 34dfee6ccb..faf179a82e 100644 --- a/plugins/tech-insights-backend/CHANGELOG.md +++ b/plugins/tech-insights-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-tech-insights-backend +## 0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/catalog-client@0.9.0-next.0 + - @backstage/plugin-tech-insights-node@0.2.7-next.0 + +## 0.2.8 + +### Patch Changes + +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + - @backstage/plugin-tech-insights-node@0.2.6 + ## 0.2.7 ### Patch Changes diff --git a/plugins/tech-insights-backend/api-report.md b/plugins/tech-insights-backend/api-report.md index 1d19123a00..d56e3c8082 100644 --- a/plugins/tech-insights-backend/api-report.md +++ b/plugins/tech-insights-backend/api-report.md @@ -11,7 +11,7 @@ import { FactCheckerFactory } from '@backstage/plugin-tech-insights-node'; import { FactLifecycle } from '@backstage/plugin-tech-insights-node'; import { FactRetriever } from '@backstage/plugin-tech-insights-node'; import { FactRetrieverRegistration } from '@backstage/plugin-tech-insights-node'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { TechInsightCheck } from '@backstage/plugin-tech-insights-node'; @@ -63,7 +63,7 @@ export interface RouterOptions< > { config: Config; factChecker?: FactChecker; - logger: Logger_2; + logger: Logger; persistenceContext: PersistenceContext; } @@ -93,7 +93,7 @@ export interface TechInsightsOptions< factCheckerFactory?: FactCheckerFactory; factRetrievers: FactRetrieverRegistration[]; // (undocumented) - logger: Logger_2; + logger: Logger; } // (No @packageDocumentation comment for this package) diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index c44aaa138c..daf102855c 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend", - "version": "0.2.7", + "version": "0.2.9-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,13 +34,13 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@backstage/plugin-tech-insights-common": "^0.2.3", - "@backstage/plugin-tech-insights-node": "^0.2.5", + "@backstage/plugin-tech-insights-node": "^0.2.7-next.0", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -54,8 +54,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.19", - "@backstage/cli": "^0.14.1", + "@backstage/backend-test-utils": "^0.1.21-next.0", + "@backstage/cli": "^0.15.2-next.0", "@types/supertest": "^2.0.8", "@types/node-cron": "^3.0.0", "@types/semver": "^7.3.8", diff --git a/plugins/tech-insights-backend/src/service/router.ts b/plugins/tech-insights-backend/src/service/router.ts index b13802358f..b86677ab90 100644 --- a/plugins/tech-insights-backend/src/service/router.ts +++ b/plugins/tech-insights-backend/src/service/router.ts @@ -27,7 +27,7 @@ import { Logger } from 'winston'; import { DateTime } from 'luxon'; import { PersistenceContext } from './persistence/persistenceContext'; import { - EntityName, + CompoundEntityRef, parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; @@ -99,8 +99,10 @@ export async function createRouter< }); router.post('/checks/run', async (req, res) => { - const { checks, entities }: { checks: string[]; entities: EntityName[] } = - req.body; + const { + checks, + entities, + }: { checks: string[]; entities: CompoundEntityRef[] } = req.body; const tasks = entities.map(async entity => { const entityTriplet = typeof entity === 'string' ? entity : stringifyEntityRef(entity); diff --git a/plugins/tech-insights-common/.eslintrc.js b/plugins/tech-insights-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/tech-insights-common/.eslintrc.js +++ b/plugins/tech-insights-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json index f20310cc0c..0b89054c50 100644 --- a/plugins/tech-insights-common/package.json +++ b/plugins/tech-insights-common/package.json @@ -38,7 +38,7 @@ "@backstage/types": "^0.1.3" }, "devDependencies": { - "@backstage/cli": "^0.14.0" + "@backstage/cli": "^0.15.2-next.0" }, "files": [ "dist" diff --git a/plugins/tech-insights-node/.eslintrc.js b/plugins/tech-insights-node/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/tech-insights-node/.eslintrc.js +++ b/plugins/tech-insights-node/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/tech-insights-node/CHANGELOG.md b/plugins/tech-insights-node/CHANGELOG.md index cb019ccb99..02ed541b56 100644 --- a/plugins/tech-insights-node/CHANGELOG.md +++ b/plugins/tech-insights-node/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-tech-insights-node +## 0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/tech-insights-node/api-report.md b/plugins/tech-insights-node/api-report.md index 77a9d77871..45426587e3 100644 --- a/plugins/tech-insights-node/api-report.md +++ b/plugins/tech-insights-node/api-report.md @@ -7,7 +7,7 @@ import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { Config } from '@backstage/config'; import { DateTime } from 'luxon'; import { DurationLike } from 'luxon'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; // @public @@ -56,7 +56,7 @@ export interface FactRetriever { export type FactRetrieverContext = { config: Config; discovery: PluginEndpointDiscovery; - logger: Logger_2; + logger: Logger; entityFilter?: | Record[] | Record; diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json index b3b0c7d6b8..8babb26bb6 100644 --- a/plugins/tech-insights-node/package.json +++ b/plugins/tech-insights-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-node", - "version": "0.2.5", + "version": "0.2.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,7 +33,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/plugin-tech-insights-common": "^0.2.3", "@types/luxon": "^2.0.5", @@ -41,7 +41,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1" + "@backstage/cli": "^0.15.2-next.0" }, "files": [ "dist" diff --git a/plugins/tech-insights-node/src/facts.ts b/plugins/tech-insights-node/src/facts.ts index 7a70f92f92..24b6764ba4 100644 --- a/plugins/tech-insights-node/src/facts.ts +++ b/plugins/tech-insights-node/src/facts.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { DateTime, DurationLike } from 'luxon'; import { Config } from '@backstage/config'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -62,11 +63,10 @@ export type TechInsightFact = { }; /** - * Response type used when returning from database and API. * Adds a field for ref for easier usage * - * @public + * @public */ export type FlatTechInsightFact = TechInsightFact & { /** @@ -76,12 +76,12 @@ export type FlatTechInsightFact = TechInsightFact & { }; /** - * @public - * * A record type to specify individual fact shapes * * Used as part of a schema to validate, identify and generically construct usage implementations * of individual fact values in the system. + * + * @public */ export type FactSchema = { /** @@ -142,11 +142,9 @@ export type FactRetrieverContext = { }; /** - * @public - * * FactRetriever interface * - * A component specifying + * @public */ export interface FactRetriever { /** @@ -191,10 +189,9 @@ export interface FactRetriever { } /** - * @public - * * A Luxon duration like object for time to live value * + * @public * @example * \{ timeToLive: 1209600000 \} * \{ timeToLive: \{ weeks: 4 \} \} @@ -203,10 +200,9 @@ export interface FactRetriever { export type TTL = { timeToLive: DurationLike }; /** - * @public - * * A maximum number for items to be kept in the database for each fact retriever/entity pair * + * @public * @example * \{ maxItems: 10 \} * @@ -214,25 +210,25 @@ export type TTL = { timeToLive: DurationLike }; export type MaxItems = { maxItems: number }; /** - * @public - * * A fact lifecycle definition. Determines which strategy to use to purge expired facts from the database. + * + * @public */ export type FactLifecycle = TTL | MaxItems; /** - * @public - * * A flat serializable structure for Facts. * Containing information about fact schema, version, id, and entity filters + * + * @public */ export type FactSchemaDefinition = Omit; /** - * @public - * * Registration of a fact retriever * Used to add and schedule individual fact retrievers to the fact retriever engine. + * + * @public */ export type FactRetrieverRegistration = { /** diff --git a/plugins/tech-insights/.eslintrc.js b/plugins/tech-insights/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/tech-insights/.eslintrc.js +++ b/plugins/tech-insights/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/tech-insights/CHANGELOG.md b/plugins/tech-insights/CHANGELOG.md index 30f21e45bd..f7785fcc71 100644 --- a/plugins/tech-insights/CHANGELOG.md +++ b/plugins/tech-insights/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-tech-insights +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.1.11 + +### Patch Changes + +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/tech-insights/api-report.md b/plugins/tech-insights/api-report.md index 79854e505f..deadd06010 100644 --- a/plugins/tech-insights/api-report.md +++ b/plugins/tech-insights/api-report.md @@ -9,7 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { BulkCheckResponse } from '@backstage/plugin-tech-insights-common'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -52,11 +52,14 @@ export interface TechInsightsApi { ) => CheckResultRenderer | undefined; // (undocumented) runBulkChecks( - entities: EntityName[], + entities: CompoundEntityRef[], checks?: Check[], ): Promise; // (undocumented) - runChecks(entityParams: EntityName, checks?: Check[]): Promise; + runChecks( + entityParams: CompoundEntityRef, + checks?: Check[], + ): Promise; } // @public diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index 99d4eeefad..acb97ed1ad 100644 --- a/plugins/tech-insights/package.json +++ b/plugins/tech-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights", - "version": "0.1.10", + "version": "0.1.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,11 +23,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@backstage/plugin-tech-insights-common": "^0.2.3", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.3", @@ -42,10 +42,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/tech-insights/src/api/TechInsightsApi.ts b/plugins/tech-insights/src/api/TechInsightsApi.ts index b1287a62dc..0a6bb937a6 100644 --- a/plugins/tech-insights/src/api/TechInsightsApi.ts +++ b/plugins/tech-insights/src/api/TechInsightsApi.ts @@ -21,7 +21,7 @@ import { } from '@backstage/plugin-tech-insights-common'; import { Check } from './types'; import { CheckResultRenderer } from '../components/CheckResultRenderer'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; /** * {@link @backstage/core-plugin-api#ApiRef} for the {@link TechInsightsApi} @@ -45,9 +45,12 @@ export interface TechInsightsApi { description?: string, ) => CheckResultRenderer | undefined; getAllChecks(): Promise; - runChecks(entityParams: EntityName, checks?: Check[]): Promise; + runChecks( + entityParams: CompoundEntityRef, + checks?: Check[], + ): Promise; runBulkChecks( - entities: EntityName[], + entities: CompoundEntityRef[], checks?: Check[], ): Promise; } diff --git a/plugins/tech-insights/src/api/TechInsightsClient.ts b/plugins/tech-insights/src/api/TechInsightsClient.ts index 5e29749854..5b9af9557f 100644 --- a/plugins/tech-insights/src/api/TechInsightsClient.ts +++ b/plugins/tech-insights/src/api/TechInsightsClient.ts @@ -22,7 +22,7 @@ import { import { Check } from './types'; import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { CheckResultRenderer, @@ -74,7 +74,7 @@ export class TechInsightsClient implements TechInsightsApi { } async runChecks( - entityParams: EntityName, + entityParams: CompoundEntityRef, checks?: Check[], ): Promise { const url = await this.discoveryApi.getBaseUrl('tech-insights'); @@ -102,7 +102,7 @@ export class TechInsightsClient implements TechInsightsApi { } async runBulkChecks( - entities: EntityName[], + entities: CompoundEntityRef[], checks?: Check[], ): Promise { const url = await this.discoveryApi.getBaseUrl('tech-insights'); diff --git a/plugins/tech-radar/.eslintrc.js b/plugins/tech-radar/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/tech-radar/.eslintrc.js +++ b/plugins/tech-radar/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index 88913e12e4..b9ebabdf50 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-tech-radar +## 0.5.9-next.0 + +### Patch Changes + +- bae72d6f4d: Tech Radar Ring names are now coloured from theme via theme.palette.text.primary (instead of a hard coded colour) +- Updated dependencies + - @backstage/core-components@0.9.1-next.0 + +## 0.5.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.5.7 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 0ea04212a5..7ed251a405 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-tech-radar", "description": "A Backstage plugin that lets you display a Tech Radar for your organization", - "version": "0.5.7", + "version": "0.5.9-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,8 +34,8 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,10 +49,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/tech-radar/src/components/RadarGrid/RadarGrid.tsx b/plugins/tech-radar/src/components/RadarGrid/RadarGrid.tsx index 94ef2b1bda..ef62b8abd9 100644 --- a/plugins/tech-radar/src/components/RadarGrid/RadarGrid.tsx +++ b/plugins/tech-radar/src/components/RadarGrid/RadarGrid.tsx @@ -23,7 +23,7 @@ export type Props = { rings: Ring[]; }; -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles(theme => ({ ring: { fill: 'none', stroke: '#bbb', @@ -37,7 +37,7 @@ const useStyles = makeStyles(() => ({ text: { pointerEvents: 'none', userSelect: 'none', - fill: '#e5e5e5', + fill: theme.palette.text.primary, fontSize: '25px', fontWeight: 800, }, diff --git a/plugins/techdocs-backend/.eslintrc.js b/plugins/techdocs-backend/.eslintrc.js index 595853b48b..d1e1c522da 100644 --- a/plugins/techdocs-backend/.eslintrc.js +++ b/plugins/techdocs-backend/.eslintrc.js @@ -1,4 +1,3 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { ignorePatterns: ['static/**'], -}; +}); diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 2e253f5d0c..0558dd098b 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-techdocs-backend +## 0.14.2-next.0 + +### Patch Changes + +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- 3c2bc73901: Use `setupRequestMockHandlers` from `@backstage/backend-test-utils` +- 3e54f6c436: Use `@backstage/plugin-search-common` package instead of `@backstage/search-common`. +- 91bf1e6c1a: Use `@backstage/plugin-techdocs-node` package instead of `@backstage/techdocs-common`. +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-techdocs-node@0.11.12-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/plugin-catalog-common@0.2.2-next.0 + - @backstage/plugin-search-common@0.3.1-next.0 + - @backstage/catalog-client@0.9.0-next.0 + +## 0.14.1 + +### Patch Changes + +- 6537a601c7: Added a new interface that allows for customization of when to build techdocs +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- 022507c860: A `DefaultTechDocsCollatorFactory`, which works with the new stream-based + search indexing subsystem, is now available. The `DefaultTechDocsCollator` will + continue to be available for those unable to upgrade to the stream-based + `@backstage/search-backend-node` (and related packages), however it is now + marked as deprecated and will be removed in a future version. + + To upgrade this plugin and the search indexing subsystem in one go, check + [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for necessary changes to your search backend plugin configuration. + +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + - @backstage/plugin-catalog-common@0.2.0 + - @backstage/integration@0.8.0 + - @backstage/search-common@0.3.0 + - @backstage/techdocs-common@0.11.11 + ## 0.14.0 ### Minor Changes diff --git a/plugins/techdocs-backend/README.md b/plugins/techdocs-backend/README.md index 57bc477f12..a1d7d22855 100644 --- a/plugins/techdocs-backend/README.md +++ b/plugins/techdocs-backend/README.md @@ -21,7 +21,7 @@ yarn start This provides serving and building of documentation for any entity. To configure various storage providers and building options, see http://backstage.io/docs/features/techdocs/configuration. -The techdocs-backend re-exports the [techdocs-common](https://github.com/backstage/backstage/tree/master/packages/techdocs-common) package which has the features to prepare, generate and publish docs. +The techdocs-backend re-exports the [techdocs-node](https://github.com/backstage/backstage/tree/master/plugins/techdocs-node) package which has the features to prepare, generate and publish docs. The Publishers are also used to fetch the static documentation files and render them in TechDocs. ## Links diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md index 5d106cfafb..157f049953 100644 --- a/plugins/techdocs-backend/api-report.md +++ b/plugins/techdocs-backend/api-report.md @@ -3,26 +3,30 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; -import { DocumentCollator } from '@backstage/search-common'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { Entity } from '@backstage/catalog-model'; import express from 'express'; -import { GeneratorBuilder } from '@backstage/techdocs-common'; +import { GeneratorBuilder } from '@backstage/plugin-techdocs-node'; import { Knex } from 'knex'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PluginCacheManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { PreparerBuilder } from '@backstage/techdocs-common'; -import { PublisherBase } from '@backstage/techdocs-common'; -import { TechDocsDocument } from '@backstage/techdocs-common'; +import { PreparerBuilder } from '@backstage/plugin-techdocs-node'; +import { PublisherBase } from '@backstage/plugin-techdocs-node'; +import { Readable } from 'stream'; +import { TechDocsDocument } from '@backstage/plugin-techdocs-node'; import { TokenManager } from '@backstage/backend-common'; // @public export function createRouter(options: RouterOptions): Promise; -// @public -export class DefaultTechDocsCollator implements DocumentCollator { +// @public @deprecated +export class DefaultTechDocsCollator { // (undocumented) protected applyArgsToFormat( format: string, @@ -41,25 +45,48 @@ export class DefaultTechDocsCollator implements DocumentCollator { readonly visibilityPermission: Permission; } +// @public +export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { + // (undocumented) + static fromConfig( + config: Config, + options: TechDocsCollatorFactoryOptions, + ): DefaultTechDocsCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type: string; + // (undocumented) + readonly visibilityPermission: Permission; +} + +// @public +export interface DocsBuildStrategy { + // (undocumented) + shouldBuild(params: ShouldBuildParameters): Promise; +} + // @public export type OutOfTheBoxDeploymentOptions = { preparers: PreparerBuilder; generators: GeneratorBuilder; publisher: PublisherBase; - logger: Logger_2; + logger: Logger; discovery: PluginEndpointDiscovery; database?: Knex; config: Config; cache: PluginCacheManager; + docsBuildStrategy?: DocsBuildStrategy; }; // @public export type RecommendedDeploymentOptions = { publisher: PublisherBase; - logger: Logger_2; + logger: Logger; discovery: PluginEndpointDiscovery; config: Config; cache: PluginCacheManager; + docsBuildStrategy?: DocsBuildStrategy; }; // @public @@ -67,10 +94,26 @@ export type RouterOptions = | RecommendedDeploymentOptions | OutOfTheBoxDeploymentOptions; +// @public +export type ShouldBuildParameters = { + entity: Entity; +}; + +// @public +export type TechDocsCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + logger: Logger; + tokenManager: TokenManager; + locationTemplate?: string; + catalogClient?: CatalogApi; + parallelismLimit?: number; + legacyPathCasing?: boolean; +}; + // @public export type TechDocsCollatorOptions = { discovery: PluginEndpointDiscovery; - logger: Logger_2; + logger: Logger; tokenManager: TokenManager; locationTemplate?: string; catalogClient?: CatalogApi; @@ -80,5 +123,5 @@ export type TechDocsCollatorOptions = { export { TechDocsDocument }; -export * from '@backstage/techdocs-common'; +export * from '@backstage/plugin-techdocs-node'; ``` diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 7e8241bfea..a233e71965 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-backend", "description": "The Backstage backend plugin that renders technical documentation for your components", - "version": "0.14.0", + "version": "0.14.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,20 +34,20 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/plugin-catalog-common": "^0.1.4", - "@backstage/search-common": "^0.2.4", - "@backstage/techdocs-common": "^0.11.10", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-catalog-common": "^0.2.2-next.0", + "@backstage/plugin-search-common": "^0.3.1-next.0", + "@backstage/plugin-techdocs-node": "^0.11.12-next.0", "@types/express": "^4.17.6", "dockerode": "^3.3.1", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "fs-extra": "9.1.0", + "fs-extra": "10.0.1", "knex": "^1.0.2", "lodash": "^4.17.21", "node-fetch": "^2.6.7", @@ -55,8 +55,9 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/backend-test-utils": "^0.1.21-next.0", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/plugin-search-backend-node": "0.5.1-next.0", "@types/dockerode": "^3.3.0", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 05e9c0ce52..09eee1fc12 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -29,7 +29,7 @@ import { PreparerBuilder, PublisherBase, UrlPreparer, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import fs from 'fs-extra'; import os from 'os'; import path from 'path'; diff --git a/plugins/techdocs-backend/src/index.ts b/plugins/techdocs-backend/src/index.ts index 12e7df1e9e..dc5d87d1b3 100644 --- a/plugins/techdocs-backend/src/index.ts +++ b/plugins/techdocs-backend/src/index.ts @@ -25,14 +25,22 @@ export type { RouterOptions, RecommendedDeploymentOptions, OutOfTheBoxDeploymentOptions, + DocsBuildStrategy, + ShouldBuildParameters, } from './service'; -export { DefaultTechDocsCollator } from './search'; -export type { TechDocsCollatorOptions } from './search'; +export { + DefaultTechDocsCollator, + DefaultTechDocsCollatorFactory, +} from './search'; +export type { + TechDocsCollatorFactoryOptions, + TechDocsCollatorOptions, +} from './search'; /** - * @deprecated Use directly from @backstage/techdocs-common + * @deprecated Use directly from @backstage/plugin-techdocs-node */ -export type { TechDocsDocument } from '@backstage/techdocs-common'; +export type { TechDocsDocument } from '@backstage/plugin-techdocs-node'; -export * from '@backstage/techdocs-common'; +export * from '@backstage/plugin-techdocs-node'; diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts index d9f75767a7..684d5729a2 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { ConfigReader } from '@backstage/config'; @@ -72,18 +72,6 @@ const expectedEntities: Entity[] = [ owner: 'someone', }, }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'test-entity', - description: 'The expected description', - }, - spec: { - type: 'some-type', - lifecycle: 'experimental', - }, - }, ]; describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => { diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts index 73c030cc51..7529eaca66 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts @@ -24,7 +24,6 @@ import { RELATION_OWNED_BY, stringifyEntityRef, } from '@backstage/catalog-model'; -import { DocumentCollator } from '@backstage/search-common'; import fetch from 'node-fetch'; import unescape from 'lodash/unescape'; import { Logger } from 'winston'; @@ -36,7 +35,7 @@ import { CatalogClient, CATALOG_FILTER_EXISTS, } from '@backstage/catalog-client'; -import { TechDocsDocument } from '@backstage/techdocs-common'; +import { TechDocsDocument } from '@backstage/plugin-techdocs-node'; interface MkSearchIndexDoc { title: string; @@ -69,8 +68,10 @@ type EntityInfo = { * A search collator responsible for gathering and transforming TechDocs documents. * * @public + * @deprecated Upgrade to a more recent `@backstage/search-backend-node` and + * use `DefaultTechDocsCollatorFactory` instead. */ -export class DefaultTechDocsCollator implements DocumentCollator { +export class DefaultTechDocsCollator { public readonly type: string = 'techdocs'; public readonly visibilityPermission = catalogEntityReadPermission; diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts new file mode 100644 index 0000000000..291f21a0d1 --- /dev/null +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts @@ -0,0 +1,245 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + getVoidLogger, + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { TestPipeline } from '@backstage/plugin-search-backend-node'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { Readable } from 'stream'; +import { DefaultTechDocsCollatorFactory } from './DefaultTechDocsCollatorFactory'; + +const logger = getVoidLogger(); + +const mockSearchDocIndex = { + config: { + lang: ['en'], + min_search_length: 3, + prebuild_index: false, + separator: '[\\s\\-]+', + }, + docs: [ + { + location: '', + text: 'docs docs docs', + title: 'Home', + }, + { + location: 'local-development/', + text: 'Docs for first subtitle', + title: 'Local development', + }, + { + location: 'local-development/#development', + text: 'Docs for sub-subtitle', + title: 'Development', + }, + ], +}; + +const expectedEntities: Entity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + title: 'Test Entity with Docs!', + name: 'test-entity-with-docs', + description: 'Documented description', + annotations: { + 'backstage.io/techdocs-ref': './', + }, + }, + spec: { + type: 'dog', + lifecycle: 'experimental', + owner: 'someone', + }, + }, +]; + +describe('DefaultTechDocsCollatorFactory', () => { + const config = new ConfigReader({}); + const mockDiscoveryApi: jest.Mocked = { + getBaseUrl: jest.fn().mockResolvedValue('http://test-backend'), + getExternalBaseUrl: jest.fn(), + }; + const mockTokenManager: jest.Mocked = { + getToken: jest.fn().mockResolvedValue({ token: '' }), + authenticate: jest.fn(), + }; + const options = { + discovery: mockDiscoveryApi, + logger: getVoidLogger(), + tokenManager: mockTokenManager, + }; + + it('has expected type', () => { + const factory = DefaultTechDocsCollatorFactory.fromConfig(config, options); + expect(factory.type).toBe('techdocs'); + }); + + describe('getCollator', () => { + let factory: DefaultTechDocsCollatorFactory; + let collator: Readable; + + const worker = setupServer(); + setupRequestMockHandlers(worker); + + beforeEach(async () => { + factory = DefaultTechDocsCollatorFactory.fromConfig(config, options); + collator = await factory.getCollator(); + + worker.use( + rest.get( + 'http://test-backend/static/docs/default/Component/test-entity-with-docs/search/search_index.json', + (_, res, ctx) => res(ctx.status(200), ctx.json(mockSearchDocIndex)), + ), + rest.get('http://test-backend/entities', (req, res, ctx) => { + // Imitate offset/limit pagination. + const offset = parseInt( + req.url.searchParams.get('offset') || '0', + 10, + ); + const limit = parseInt( + req.url.searchParams.get('limit') || '500', + 10, + ); + + // Limit 50 corresponds to a case testing pagination. + if (limit === 50) { + // Return 50 copies of invalid entities on the first request. + if (offset === 0) { + return res(ctx.status(200), ctx.json(Array(50).fill({}))); + } + // Then just the regular 2 on the second. + return res(ctx.status(200), ctx.json(expectedEntities)); + } + return res( + ctx.status(200), + ctx.json(expectedEntities.slice(offset, limit + offset)), + ); + }), + ); + }); + + it('returns a readable stream', async () => { + expect(collator).toBeInstanceOf(Readable); + }); + + it('fetches from the configured catalog and tech docs services', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('catalog'); + expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('techdocs'); + expect(documents).toHaveLength(mockSearchDocIndex.docs.length); + }); + + it('should create documents for each tech docs search index', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + const entity = expectedEntities[0]; + documents.forEach((document, idx) => { + expect(document).toMatchObject({ + title: mockSearchDocIndex.docs[idx].title, + location: `/docs/default/component/${entity.metadata.name}/${mockSearchDocIndex.docs[idx].location}`, + text: mockSearchDocIndex.docs[idx].text, + namespace: 'default', + entityTitle: entity!.metadata.title, + componentType: entity!.spec!.type, + lifecycle: entity!.spec!.lifecycle, + owner: '', + kind: entity.kind.toLocaleLowerCase('en-US'), + name: entity.metadata.name, + }); + }); + }); + + it('maps a returned entity with a custom locationTemplate', async () => { + // Provide an alternate location template. + factory = DefaultTechDocsCollatorFactory.fromConfig(config, { + discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, + locationTemplate: '/software/:name', + logger, + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + expect(documents[0]).toMatchObject({ + location: '/software/test-entity-with-docs', + }); + }); + + it('paginates through catalog entities using batchSize', async () => { + // A parallelismLimit of 1 is a catalog limit of 50 per request. Code + // above in the /entities handler ensures valid entities are only + // returned on the second page. + factory = DefaultTechDocsCollatorFactory.fromConfig(config, { + ...options, + parallelismLimit: 1, + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + // Only 1 entity with TechDocs configured multipled by 3 pages. + expect(documents).toHaveLength(3); + }); + + describe('with legacyPathCasing configuration', () => { + beforeEach(async () => { + const legacyConfig = new ConfigReader({ + techdocs: { + legacyUseCaseSensitiveTripletPaths: true, + }, + }); + factory = DefaultTechDocsCollatorFactory.fromConfig( + legacyConfig, + options, + ); + collator = await factory.getCollator(); + }); + + it('should create documents for each tech docs search index', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + const entity = expectedEntities[0]; + documents.forEach((document, idx) => { + expect(document).toMatchObject({ + title: mockSearchDocIndex.docs[idx].title, + location: `/docs/default/Component/${entity.metadata.name}/${mockSearchDocIndex.docs[idx].location}`, + text: mockSearchDocIndex.docs[idx].text, + namespace: 'default', + entityTitle: entity!.metadata.title, + componentType: entity!.spec!.type, + lifecycle: entity!.spec!.lifecycle, + owner: '', + kind: entity.kind, + name: entity.metadata.name, + }); + }); + }); + }); + }); +}); diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts new file mode 100644 index 0000000000..41d637e319 --- /dev/null +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts @@ -0,0 +1,254 @@ +/* + * Copyright 2022 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 { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { + CatalogApi, + CatalogClient, + CATALOG_FILTER_EXISTS, +} from '@backstage/catalog-client'; +import { + Entity, + parseEntityRef, + RELATION_OWNED_BY, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { TechDocsDocument } from '@backstage/plugin-techdocs-node'; +import unescape from 'lodash/unescape'; +import fetch from 'node-fetch'; +import pLimit from 'p-limit'; +import { Readable } from 'stream'; +import { Logger } from 'winston'; + +interface MkSearchIndexDoc { + title: string; + text: string; + location: string; +} + +/** + * Options to configure the TechDocs collator factory + * + * @public + */ +export type TechDocsCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + logger: Logger; + tokenManager: TokenManager; + locationTemplate?: string; + catalogClient?: CatalogApi; + parallelismLimit?: number; + legacyPathCasing?: boolean; +}; + +type EntityInfo = { + name: string; + namespace: string; + kind: string; +}; + +/** + * A search collator factory responsible for gathering and transforming + * TechDocs documents. + * + * @public + */ +export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'techdocs'; + public readonly visibilityPermission = catalogEntityReadPermission; + + private discovery: PluginEndpointDiscovery; + private locationTemplate: string; + private readonly logger: Logger; + private readonly catalogClient: CatalogApi; + private readonly tokenManager: TokenManager; + private readonly parallelismLimit: number; + private readonly legacyPathCasing: boolean; + + private constructor(options: TechDocsCollatorFactoryOptions) { + this.discovery = options.discovery; + this.locationTemplate = + options.locationTemplate || '/docs/:namespace/:kind/:name/:path'; + this.logger = options.logger; + this.catalogClient = + options.catalogClient || + new CatalogClient({ discoveryApi: options.discovery }); + this.parallelismLimit = options.parallelismLimit ?? 10; + this.legacyPathCasing = options.legacyPathCasing ?? false; + this.tokenManager = options.tokenManager; + } + + static fromConfig(config: Config, options: TechDocsCollatorFactoryOptions) { + const legacyPathCasing = + config.getOptionalBoolean( + 'techdocs.legacyUseCaseSensitiveTripletPaths', + ) || false; + return new DefaultTechDocsCollatorFactory({ ...options, legacyPathCasing }); + } + + async getCollator(): Promise { + return Readable.from(this.execute()); + } + + private async *execute(): AsyncGenerator { + const limit = pLimit(this.parallelismLimit); + const techDocsBaseUrl = await this.discovery.getBaseUrl('techdocs'); + const { token } = await this.tokenManager.getToken(); + let entitiesRetrieved = 0; + let moreEntitiesToGet = true; + + // Offset/limit pagination is used on the Catalog Client in order to + // limit (and allow some control over) memory used by the search backend + // at index-time. The batchSize is calculated as a factor of the given + // parallelism limit to simplify configuration. + const batchSize = this.parallelismLimit * 50; + while (moreEntitiesToGet) { + const entities = ( + await this.catalogClient.getEntities( + { + filter: { + 'metadata.annotations.backstage.io/techdocs-ref': + CATALOG_FILTER_EXISTS, + }, + fields: [ + 'kind', + 'namespace', + 'metadata.annotations', + 'metadata.name', + 'metadata.title', + 'metadata.namespace', + 'spec.type', + 'spec.lifecycle', + 'relations', + ], + limit: batchSize, + offset: entitiesRetrieved, + }, + { token }, + ) + ).items; + + // Control looping through entity batches. + moreEntitiesToGet = entities.length === batchSize; + entitiesRetrieved += entities.length; + + const docPromises = entities + .filter(it => it.metadata?.annotations?.['backstage.io/techdocs-ref']) + .map((entity: Entity) => + limit(async (): Promise => { + const entityInfo = + DefaultTechDocsCollatorFactory.handleEntityInfoCasing( + this.legacyPathCasing, + { + kind: entity.kind, + namespace: entity.metadata.namespace || 'default', + name: entity.metadata.name, + }, + ); + + try { + const searchIndexResponse = await fetch( + DefaultTechDocsCollatorFactory.constructDocsIndexUrl( + techDocsBaseUrl, + entityInfo, + ), + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + const searchIndex = await searchIndexResponse.json(); + + return searchIndex.docs.map((doc: MkSearchIndexDoc) => ({ + title: unescape(doc.title), + text: unescape(doc.text || ''), + location: this.applyArgsToFormat( + this.locationTemplate || '/docs/:namespace/:kind/:name/:path', + { + ...entityInfo, + path: doc.location, + }, + ), + path: doc.location, + ...entityInfo, + entityTitle: entity.metadata.title, + componentType: entity.spec?.type?.toString() || 'other', + lifecycle: (entity.spec?.lifecycle as string) || '', + owner: getSimpleEntityOwnerString(entity), + authorization: { + resourceRef: stringifyEntityRef(entity), + }, + })); + } catch (e) { + this.logger.debug( + `Failed to retrieve tech docs search index for entity ${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}`, + e, + ); + return []; + } + }), + ); + yield* (await Promise.all(docPromises)).flat(); + } + } + + private applyArgsToFormat( + format: string, + args: Record, + ): string { + let formatted = format; + for (const [key, value] of Object.entries(args)) { + formatted = formatted.replace(`:${key}`, value); + } + return formatted; + } + + private static constructDocsIndexUrl( + techDocsBaseUrl: string, + entityInfo: { kind: string; namespace: string; name: string }, + ) { + return `${techDocsBaseUrl}/static/docs/${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}/search/search_index.json`; + } + + private static handleEntityInfoCasing( + legacyPaths: boolean, + entityInfo: EntityInfo, + ): EntityInfo { + return legacyPaths + ? entityInfo + : Object.entries(entityInfo).reduce((acc, [key, value]) => { + return { ...acc, [key]: value.toLocaleLowerCase('en-US') }; + }, {} as EntityInfo); + } +} + +function getSimpleEntityOwnerString(entity: Entity): string { + if (entity.relations) { + const owner = entity.relations.find(r => r.type === RELATION_OWNED_BY); + if (owner) { + const { name } = parseEntityRef(owner.targetRef); + return name; + } + } + return ''; +} diff --git a/plugins/techdocs-backend/src/search/index.ts b/plugins/techdocs-backend/src/search/index.ts index fbbd23b964..68e3b4edc7 100644 --- a/plugins/techdocs-backend/src/search/index.ts +++ b/plugins/techdocs-backend/src/search/index.ts @@ -13,6 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; +export { DefaultTechDocsCollatorFactory } from './DefaultTechDocsCollatorFactory'; +export type { TechDocsCollatorFactoryOptions } from './DefaultTechDocsCollatorFactory'; + +/** + * todo(backstage/techdocs-core): stop exporting these in a future release. + */ +export { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; export type { TechDocsCollatorOptions } from './DefaultTechDocsCollator'; diff --git a/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts b/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts index 678a4b0539..19684486dc 100644 --- a/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts +++ b/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts @@ -16,11 +16,11 @@ import { CachedEntityLoader } from './CachedEntityLoader'; import { CatalogClient } from '@backstage/catalog-client'; import { CacheClient } from '@backstage/backend-common'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; describe('CachedEntityLoader', () => { const catalog: jest.Mocked = { - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), } as any; const cache: jest.Mocked = { @@ -28,7 +28,7 @@ describe('CachedEntityLoader', () => { set: jest.fn(), } as any; - const entityName: EntityName = { + const entityName: CompoundEntityRef = { kind: 'component', namespace: 'default', name: 'test', @@ -53,7 +53,7 @@ describe('CachedEntityLoader', () => { it('writes entities to cache', async () => { cache.get.mockResolvedValue(undefined); - catalog.getEntityByName.mockResolvedValue(entity); + catalog.getEntityByRef.mockResolvedValue(entity); const result = await loader.load(entityName, token); @@ -71,12 +71,12 @@ describe('CachedEntityLoader', () => { const result = await loader.load(entityName, token); expect(result).toEqual(entity); - expect(catalog.getEntityByName).not.toBeCalled(); + expect(catalog.getEntityByRef).not.toBeCalled(); }); it('does not cache missing entites', async () => { cache.get.mockResolvedValue(undefined); - catalog.getEntityByName.mockResolvedValue(undefined); + catalog.getEntityByRef.mockResolvedValue(undefined); const result = await loader.load(entityName, token); @@ -86,7 +86,7 @@ describe('CachedEntityLoader', () => { it('uses entity ref as cache key for anonymous users', async () => { cache.get.mockResolvedValue(undefined); - catalog.getEntityByName.mockResolvedValue(entity); + catalog.getEntityByRef.mockResolvedValue(entity); const result = await loader.load(entityName, undefined); @@ -103,7 +103,7 @@ describe('CachedEntityLoader', () => { setTimeout(() => resolve(undefined), 10000); }), ); - catalog.getEntityByName.mockResolvedValue(entity); + catalog.getEntityByRef.mockResolvedValue(entity); const result = await loader.load(entityName, token); diff --git a/plugins/techdocs-backend/src/service/CachedEntityLoader.ts b/plugins/techdocs-backend/src/service/CachedEntityLoader.ts index 81c6ec7431..424d9b9541 100644 --- a/plugins/techdocs-backend/src/service/CachedEntityLoader.ts +++ b/plugins/techdocs-backend/src/service/CachedEntityLoader.ts @@ -17,7 +17,7 @@ import { CatalogClient } from '@backstage/catalog-client'; import { CacheClient } from '@backstage/backend-common'; import { Entity, - EntityName, + CompoundEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; @@ -37,17 +37,17 @@ export class CachedEntityLoader { } async load( - entityName: EntityName, + entityRef: CompoundEntityRef, token: string | undefined, ): Promise { - const cacheKey = this.getCacheKey(entityName, token); + const cacheKey = this.getCacheKey(entityRef, token); let result = await this.getFromCache(cacheKey); if (result) { return result; } - result = await this.catalog.getEntityByName(entityName, { token }); + result = await this.catalog.getEntityByRef(entityRef, { token }); if (result) { this.cache.set(cacheKey, result, { ttl: 5000 }); @@ -66,7 +66,7 @@ export class CachedEntityLoader { } private getCacheKey( - entityName: EntityName, + entityName: CompoundEntityRef, token: string | undefined, ): string { const key = ['catalog', stringifyEntityRef(entityName)]; diff --git a/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts b/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts new file mode 100644 index 0000000000..84bd960f8f --- /dev/null +++ b/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2022 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 { DefaultDocsBuildStrategy } from './DocsBuildStrategy'; +import { ConfigReader } from '@backstage/config'; + +const MockedConfigReader = ConfigReader as jest.MockedClass< + typeof ConfigReader +>; + +jest.mock('@backstage/config'); + +describe('DefaultDocsBuildStrategy', () => { + const entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + uid: '0', + name: 'test', + }, + }; + + const config = new ConfigReader({}); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('shouldBuild', () => { + it('should return true when techdocs.build is set to local', async () => { + const defaultDocsBuildStrategy = + DefaultDocsBuildStrategy.fromConfig(config); + + MockedConfigReader.prototype.getString.mockReturnValue('local'); + + const result = await defaultDocsBuildStrategy.shouldBuild({ entity }); + + expect(result).toBe(true); + }); + + it('should return false when techdocs.build is set to external', async () => { + const defaultDocsBuildStrategy = + DefaultDocsBuildStrategy.fromConfig(config); + + MockedConfigReader.prototype.getString.mockReturnValue('external'); + + const result = await defaultDocsBuildStrategy.shouldBuild({ entity }); + + expect(result).toBe(false); + }); + }); +}); diff --git a/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts b/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts new file mode 100644 index 0000000000..42a16234f9 --- /dev/null +++ b/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Entity } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; + +/** + * Parameters passed to the shouldBuild method on the DocsBuildStrategy interface + * + * @public + */ +export type ShouldBuildParameters = { + entity: Entity; +}; + +/** + * A strategy for when to build TechDocs locally, and when to skip building TechDocs (allowing for an external build) + * + * @public + */ +export interface DocsBuildStrategy { + shouldBuild(params: ShouldBuildParameters): Promise; +} + +export class DefaultDocsBuildStrategy { + private readonly config: Config; + + private constructor(config: Config) { + this.config = config; + } + + static fromConfig(config: Config): DefaultDocsBuildStrategy { + return new DefaultDocsBuildStrategy(config); + } + + async shouldBuild(_: ShouldBuildParameters): Promise { + return this.config.getString('techdocs.builder') === 'local'; + } +} diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts index 1830dbfcf0..05dfb5bdcc 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts @@ -24,7 +24,7 @@ import { GeneratorBuilder, PreparerBuilder, PublisherBase, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import { TechDocsCache } from '../cache'; import { DocsBuilder, shouldCheckForUpdate } from '../DocsBuilder'; import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts index 2979d2efe4..090efc4639 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts @@ -23,7 +23,7 @@ import { GeneratorBuilder, PreparerBuilder, PublisherBase, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import fetch from 'node-fetch'; import { PassThrough } from 'stream'; import * as winston from 'winston'; diff --git a/plugins/techdocs-backend/src/service/index.ts b/plugins/techdocs-backend/src/service/index.ts index 0065e33a2a..7355a34e32 100644 --- a/plugins/techdocs-backend/src/service/index.ts +++ b/plugins/techdocs-backend/src/service/index.ts @@ -20,3 +20,7 @@ export type { RecommendedDeploymentOptions, OutOfTheBoxDeploymentOptions, } from './router'; +export type { + DocsBuildStrategy, + ShouldBuildParameters, +} from './DocsBuildStrategy'; diff --git a/plugins/techdocs-backend/src/service/router.test.ts b/plugins/techdocs-backend/src/service/router.test.ts index 43e9641efb..2e707c9513 100644 --- a/plugins/techdocs-backend/src/service/router.test.ts +++ b/plugins/techdocs-backend/src/service/router.test.ts @@ -26,7 +26,7 @@ import { GeneratorBuilder, PreparerBuilder, PublisherBase, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import express, { Response } from 'express'; import request from 'supertest'; import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; @@ -38,6 +38,7 @@ import { RouterOptions, } from './router'; import { TechDocsCache } from '../cache'; +import { DocsBuildStrategy } from './DocsBuildStrategy'; jest.mock('@backstage/catalog-client'); jest.mock('@backstage/config'); @@ -120,6 +121,9 @@ describe('createRouter', () => { const cache: jest.Mocked = { getClient: jest.fn(), }; + const docsBuildStrategy: jest.Mocked = { + shouldBuild: jest.fn(), + }; const outOfTheBoxOptions = { preparers, generators, @@ -128,6 +132,7 @@ describe('createRouter', () => { logger: getVoidLogger(), discovery, cache, + docsBuildStrategy, }; const recommendedOptions = { publisher, @@ -135,6 +140,7 @@ describe('createRouter', () => { logger: getVoidLogger(), discovery, cache, + docsBuildStrategy, }; beforeEach(() => { @@ -181,10 +187,10 @@ describe('createRouter', () => { expect(response.status).toBe(404); }); - it('should not check for an update without local builder', async () => { + it('should not check for an update when shouldBuild returns false', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('external'); + docsBuildStrategy.shouldBuild.mockResolvedValue(false); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doCacheSync.mockImplementation( async ({ responseHandler }) => @@ -198,10 +204,10 @@ describe('createRouter', () => { expect(response.status).toBe(304); }); - it('should error if missing builder', async () => { + it('should error if build is required and is missing preparer', async () => { const app = await createApp(recommendedOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); const response = await request(app) @@ -210,7 +216,7 @@ describe('createRouter', () => { expect(response.status).toBe(500); expect(response.text).toMatch( - /Invalid configuration\. 'techdocs\.builder' was set to 'local' but no 'preparer' was provided to the router initialization/, + /Invalid configuration\. docsBuildStrategy\.shouldBuild returned 'true', but no 'preparer' was provided to the router initialization./, ); expect(MockDocsSynchronizer.prototype.doSync).toBeCalledTimes(0); @@ -219,7 +225,7 @@ describe('createRouter', () => { it('should execute synchronization', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doSync.mockImplementation( async ({ responseHandler }) => @@ -244,7 +250,7 @@ describe('createRouter', () => { it('should return on updated', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doSync.mockImplementation( async ({ responseHandler }) => { @@ -297,10 +303,10 @@ describe('createRouter', () => { expect(response.status).toBe(404); }); - it('should not check for an update without local builder', async () => { + it('should not check for an update when shouldBuild returns false', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('external'); + docsBuildStrategy.shouldBuild.mockResolvedValue(false); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doCacheSync.mockImplementation( async ({ responseHandler }) => @@ -322,10 +328,10 @@ data: {"updated":false} ); }); - it('should error if missing builder', async () => { + it('should error if build is required and is missing preparer', async () => { const app = await createApp(recommendedOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); const response = await request(app) @@ -337,7 +343,7 @@ data: {"updated":false} expect(response.get('content-type')).toBe('text/event-stream'); expect(response.text).toEqual( `event: error -data: "Invalid configuration. 'techdocs.builder' was set to 'local' but no 'preparer' was provided to the router initialization." +data: "Invalid configuration. docsBuildStrategy.shouldBuild returned 'true', but no 'preparer' was provided to the router initialization." `, ); @@ -348,7 +354,7 @@ data: "Invalid configuration. 'techdocs.builder' was set to 'local' but no 'prep it('should execute synchronization', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doSync.mockImplementation( async ({ responseHandler }) => @@ -376,7 +382,7 @@ data: "Invalid configuration. 'techdocs.builder' was set to 'local' but no 'prep it('should return an event-stream', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doSync.mockImplementation( async ({ responseHandler }) => { diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 5877dda099..e404c1b6d2 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -26,7 +26,7 @@ import { getLocationForEntity, PreparerBuilder, PublisherBase, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import express, { Response } from 'express'; import Router from 'express-promise-router'; import { Knex } from 'knex'; @@ -35,6 +35,10 @@ import { ScmIntegrations } from '@backstage/integration'; import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; import { createCacheMiddleware, TechDocsCache } from '../cache'; import { CachedEntityLoader } from './CachedEntityLoader'; +import { + DefaultDocsBuildStrategy, + DocsBuildStrategy, +} from './DocsBuildStrategy'; /** * Required dependencies for running TechDocs in the "out-of-the-box" @@ -51,6 +55,7 @@ export type OutOfTheBoxDeploymentOptions = { database?: Knex; // TODO: Make database required when we're implementing database stuff. config: Config; cache: PluginCacheManager; + docsBuildStrategy?: DocsBuildStrategy; }; /** @@ -65,6 +70,7 @@ export type RecommendedDeploymentOptions = { discovery: PluginEndpointDiscovery; config: Config; cache: PluginCacheManager; + docsBuildStrategy?: DocsBuildStrategy; }; /** @@ -99,6 +105,8 @@ export async function createRouter( const router = Router(); const { publisher, config, logger, discovery } = options; const catalogClient = new CatalogClient({ discoveryApi: discovery }); + const docsBuildStrategy = + options.docsBuildStrategy ?? DefaultDocsBuildStrategy.fromConfig(config); // Entities are cached to optimize the /static/docs request path, which can be called many times // when loading a single techdocs page. @@ -210,10 +218,13 @@ export async function createRouter( responseHandler = createEventStream(res); } - // techdocs-backend will only try to build documentation for an entity if techdocs.builder is set to 'local' - // If set to 'external', it will assume that an external process (e.g. CI/CD pipeline - // of the repository) is responsible for building and publishing documentation to the storage provider - if (config.getString('techdocs.builder') !== 'local') { + // By default, techdocs-backend will only try to build documentation for an entity if techdocs.builder is set to + // 'local'. If set to 'external', it will assume that an external process (e.g. CI/CD pipeline + // of the repository) is responsible for building and publishing documentation to the storage provider. + // Altering the implementation of the injected docsBuildStrategy allows for more complex behaviours, based on + // either config or the properties of the entity (e.g. annotations, labels, spec fields etc.). + const shouldBuild = await docsBuildStrategy.shouldBuild({ entity }); + if (!shouldBuild) { // However, if caching is enabled, take the opportunity to check and // invalidate stale cache entries. if (cache) { @@ -244,7 +255,7 @@ export async function createRouter( responseHandler.error( new Error( - "Invalid configuration. 'techdocs.builder' was set to 'local' but no 'preparer' was provided to the router initialization.", + "Invalid configuration. docsBuildStrategy.shouldBuild returned 'true', but no 'preparer' was provided to the router initialization.", ), ); }); @@ -336,8 +347,8 @@ export function createEventStream( } /** - * @deprecated use event-stream implementation of the sync endpoint - * */ + * @deprecated use event-stream implementation of the sync endpoint + */ export function createHttpResponse( res: Response, ): DocsSynchronizerSyncOpts { diff --git a/plugins/techdocs-backend/src/service/standaloneServer.ts b/plugins/techdocs-backend/src/service/standaloneServer.ts index eeb85bfad0..4ab4c084e0 100644 --- a/plugins/techdocs-backend/src/service/standaloneServer.ts +++ b/plugins/techdocs-backend/src/service/standaloneServer.ts @@ -28,7 +28,7 @@ import { Preparers, Publisher, TechdocsGenerator, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import Docker from 'dockerode'; import { Server } from 'http'; import { Logger } from 'winston'; diff --git a/plugins/techdocs-node/.eslintrc.js b/plugins/techdocs-node/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/techdocs-node/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/techdocs-node/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md new file mode 100644 index 0000000000..dd6cd85afd --- /dev/null +++ b/plugins/techdocs-node/CHANGELOG.md @@ -0,0 +1,1077 @@ +# @backstage/plugin-techdocs-node + +## 0.11.12-next.0 + +### Patch Changes + +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- 3e54f6c436: Use `@backstage/plugin-search-common` package instead of `@backstage/search-common`. +- cea6f10b97: Renamed `@backstage/techdocs-common` to `@backstage/plugin-techdocs-node`. +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/plugin-search-common@0.3.1-next.0 + +## 0.11.11 + +### Patch Changes + +- 955be6bc7d: adds passing projectID to the Storage client +- ff0a16fb1a: Modify techdocs builder to automatically append techdocs-core plugin to mkdocs.yaml file if it is missing. Adds an optional configuration item if this plugin needs to be omitted. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/backend-common@0.12.0 + - @backstage/integration@0.8.0 + - @backstage/search-common@0.3.0 + +## 0.11.10 + +### Patch Changes + +- 209fd128e6: Updated usage of `github:` location types in docs to use `url:` instead. +- 13ef228d03: Clean up the API interface for TechDocs common library. +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + - @backstage/integration@0.7.5 + +## 0.11.9 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/integration@0.7.4 + - @backstage/search-common@0.2.4 + +## 0.11.8 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 216725b434: Updated to use new names for `parseLocationRef` and `stringifyLocationRef` +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/search-common@0.2.3 + +## 0.11.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7 + +## 0.11.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.11.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.11.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + +## 0.11.5 + +### Patch Changes + +- ff93fbeeec: Fix interpolated string for "Failed to generate docs from ..." +- Updated dependencies + - @backstage/search-common@0.2.2 + - @backstage/backend-common@0.10.5 + +## 0.11.4 + +### Patch Changes + +- 47277c0d8c: Updated the default version of the `@spotify/techdocs` container used when `techdocs.generator.runIn` is `docker` to `v0.3.6`, which includes an update to `mkdocs-monorepo-plugin` that allows glob-based wildcard includes. +- Updated dependencies + - @backstage/integration@0.7.2 + - @backstage/backend-common@0.10.4 + - @backstage/config@0.1.13 + - @backstage/catalog-model@0.9.10 + +## 0.11.4-next.0 + +### Patch Changes + +- 47277c0d8c: Updated the default version of the `@spotify/techdocs` container used when `techdocs.generator.runIn` is `docker` to `v0.3.6`, which includes an update to `mkdocs-monorepo-plugin` that allows glob-based wildcard includes. +- Updated dependencies + - @backstage/backend-common@0.10.4-next.0 + - @backstage/config@0.1.13-next.0 + - @backstage/catalog-model@0.9.10-next.0 + - @backstage/integration@0.7.2-next.0 + +## 0.11.3 + +### Patch Changes + +- 5333451def: Cleaned up API exports +- Updated dependencies + - @backstage/config@0.1.12 + - @backstage/integration@0.7.1 + - @backstage/backend-common@0.10.3 + - @backstage/errors@0.2.0 + - @backstage/catalog-model@0.9.9 + +## 0.11.2 + +### Patch Changes + +- c2c8768771: Bump `@azure/identity` from `^1.5.0` to `^2.0.1`. +- Updated dependencies + - @backstage/backend-common@0.10.1 + - @backstage/integration@0.7.0 + +## 0.11.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.0 + +## 0.11.0 + +### Minor Changes + +- 1bada775a9: Added the ability for the TechDocs Backend to (optionally) leverage a cache + store to improve performance when reading files from a cloud storage provider. + +### Patch Changes + +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/backend-common@0.9.13 + +## 0.10.8 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/backend-common@0.9.11 + +## 0.10.7 + +### Patch Changes + +- 0b60a051c9: Added OpenStack Swift case migration support. +- 9e64a7ac1e: Allow amazon web services s3 buckets to pass an server side encryption configuration so they can publish to encrypted buckets +- Updated dependencies + - @backstage/catalog-model@0.9.7 + - @backstage/backend-common@0.9.10 + +## 0.10.6 + +### Patch Changes + +- a2d4389587: 1. Techdocs publisher constructors now use parameter objects when being + instantiated + + 2. Internal refactor of `LocalPublish` publisher to use `fromConfig` for + creation to be aligned with other publishers; this does not impact + `LocalPublish` usage. + + ```diff + - const publisher = new LocalPublish(config, logger, discovery); + + const publisher = LocalPublish.fromConfig(config, logger, discovery); + ``` + +- 6129c89a47: Default TechDocs container used at docs generation-time is now [v0.3.5](https://github.com/backstage/techdocs-container/releases/tag/v0.3.5). +- f3c7eec64b: Updated to properly join URL segments under any OS for both AWS S3 and GCP +- Updated dependencies + - @backstage/backend-common@0.9.9 + +## 0.10.5 + +### Patch Changes + +- d207f6ee9e: Support optional bucketRootPath configuration parameter in S3 and GCS publishers +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/catalog-model@0.9.6 + - @backstage/search-common@0.2.1 + +## 0.10.4 + +### Patch Changes + +- 87f5b9db13: Use docs/README.md or README.md as fallback if docs/index.md is missing +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/backend-common@0.9.7 + - @backstage/errors@0.1.3 + - @backstage/catalog-model@0.9.5 + +## 0.10.3 + +### Patch Changes + +- 156421c59a: Sets the default techdocs docker image to the [latest released version - v0.3.3](https://github.com/backstage/techdocs-container/releases/tag/v0.3.3). +- Updated dependencies + - @backstage/catalog-model@0.9.4 + - @backstage/backend-common@0.9.6 + - @backstage/integration@0.6.7 + +## 0.10.2 + +### Patch Changes + +- 1c75e8bf98: Add more context to techdocs log lines when files are not found along with + ensuring that the routers return 404 with a descriptive message. +- e92f0f728b: Locks the version of the default docker image used to generate TechDocs. As of + this changelog entry, it is v0.3.2! +- Updated dependencies + - @backstage/backend-common@0.9.5 + - @backstage/integration@0.6.6 + +## 0.10.1 + +### Patch Changes + +- 96fef17a18: Upgrade git-parse-url to v11.6.0 +- Updated dependencies + - @backstage/backend-common@0.9.3 + - @backstage/integration@0.6.4 + +## 0.10.0 + +### Minor Changes + +- 8b0f6f860: Set the correct `edit_uri` or `repo_url` for documentation pages that are hosted on GitHub and GitLab. + + The constructor of the `TechDocsGenerator` changed. + Prefer the use of `TechdocsGenerator.fromConfig(…)` instead: + + ```diff + - const techdocsGenerator = new TechdocsGenerator({ + + const techdocsGenerator = TechdocsGenerator.fromConfig(config, { + logger, + containerRunner, + - config, + }); + ``` + +### Patch Changes + +- 30ed662a3: Adding in-context search to TechDocs Reader component. Using existing search-backend to query for indexed search results scoped into a specific entity's techdocs. Needs TechDocsCollator enabled on the backend to work. + + Adding extra information to indexed tech docs documents for search. + +- 3624616e7: "Local" (out-of-the-box) publisher explicitly follows lower-case entity triplet + logic. +- 67ba7e088: Only write the updated `mkdocs.yml` file if the content was updated. + + This keeps local files unchanged if the `dir` annotation is used in combination with the `file` location. + +- 8eab6be6a: Force using `posix` path for cloud storage +- Updated dependencies + - @backstage/integration@0.6.3 + - @backstage/search-common@0.2.0 + - @backstage/catalog-model@0.9.1 + - @backstage/backend-common@0.9.1 + +## 0.9.0 + +### Minor Changes + +- 58452cdb7: OpenStack Swift Client changed with Trendyol's OpenStack Swift SDK. + + ## Migration from old OpenStack Swift Configuration + + Let's assume we have the old OpenStack Swift configuration here. + + ```yaml + techdocs: + publisher: + type: 'openStackSwift' + openStackSwift: + containerName: 'name-of-techdocs-storage-bucket' + credentials: + username: ${OPENSTACK_SWIFT_STORAGE_USERNAME} + password: ${OPENSTACK_SWIFT_STORAGE_PASSWORD} + authUrl: ${OPENSTACK_SWIFT_STORAGE_AUTH_URL} + keystoneAuthVersion: ${OPENSTACK_SWIFT_STORAGE_AUTH_VERSION} + domainId: ${OPENSTACK_SWIFT_STORAGE_DOMAIN_ID} + domainName: ${OPENSTACK_SWIFT_STORAGE_DOMAIN_NAME} + region: ${OPENSTACK_SWIFT_STORAGE_REGION} + ``` + + ##### Step 1: Change the credential keys + + Since the new SDK uses _Application Credentials_ to authenticate OpenStack, we + need to change the keys `credentials.username` to `credentials.id`, + `credentials.password` to `credentials.secret` and use Application Credential ID + and secret here. For more detail about credentials look + [here](https://docs.openstack.org/api-ref/identity/v3/?expanded=password-authentication-with-unscoped-authorization-detail,authenticating-with-an-application-credential-detail#authenticating-with-an-application-credential). + + ##### Step 2: Remove the unused keys + + Since the new SDK doesn't use the old way authentication, we don't need the keys + `openStackSwift.keystoneAuthVersion`, `openStackSwift.domainId`, + `openStackSwift.domainName` and `openStackSwift.region`. So you can remove them. + + ##### Step 3: Add Swift URL + + The new SDK needs the OpenStack Swift connection URL for connecting the Swift. + So you need to add a new key called `openStackSwift.swiftUrl` and give the + OpenStack Swift url here. Example url should look like that: + `https://example.com:6780/swift/v1` + + ##### That's it! + + Your new configuration should look like that! + + ```yaml + techdocs: + publisher: + type: 'openStackSwift' + openStackSwift: + containerName: 'name-of-techdocs-storage-bucket' + credentials: + id: ${OPENSTACK_SWIFT_STORAGE_APPLICATION_CREDENTIALS_ID} + secret: ${OPENSTACK_SWIFT_STORAGE_APPLICATION_CREDENTIALS_SECRET} + authUrl: ${OPENSTACK_SWIFT_STORAGE_AUTH_URL} + swiftUrl: ${OPENSTACK_SWIFT_STORAGE_SWIFT_URL} + ``` + +- c772d9a84: TechDocs sites can now be accessed using paths containing entity triplets of + any case (e.g. `/docs/namespace/KIND/name` or `/docs/namespace/kind/name`). + + If you do not use an external storage provider for serving TechDocs, this is a + transparent change and no action is required from you. + + If you _do_ use an external storage provider for serving TechDocs (one of\* GCS, + AWS S3, or Azure Blob Storage), you must run a migration command against your + storage provider before updating. + + [A migration guide is available here](https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-alpha-to-beta). + + - (\*) We're seeking help from the community to bring OpenStack Swift support + [to feature parity](https://github.com/backstage/backstage/issues/6763) with the above. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.9.0 + - @backstage/integration@0.6.2 + - @backstage/config@0.1.8 + +## 0.8.1 + +### Patch Changes + +- bc405be6e: Stale TechDocs content (files that had previously been published but which have + since been removed) is now removed from storage at publish-time. This is now + supported by the following publishers: + + - Google GCS + - AWS S3 + - Azure Blob Storage + + You may need to apply a greater level of permissions (e.g. the ability to + delete objects in your storage provider) to any credentials/accounts used by + the TechDocs CLI or TechDocs backend in order for this change to take effect. + + For more details, see [#6132][issue-ref]. + + [issue-ref]: https://github.com/backstage/backstage/issues/6132 + +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/backend-common@0.8.9 + +## 0.8.0 + +### Minor Changes + +- 48ea3d25b: TechDocs has dropped all support for the long-ago deprecated git-based common + prepares as well as all corresponding values in `backstage.io/techdocs-ref` + annotations. + + Entities whose `backstage.io/techdocs-ref` annotation values still begin with + `github:`, `gitlab:`, `bitbucket:`, or `azure/api:` will no longer be generated + by TechDocs. Be sure to update these values so that they align with their + expected format and your usage of TechDocs. + + For details, see [this explainer on TechDocs ref annotation values][how]. + + [how]: https://backstage.io/docs/features/techdocs/how-to-guides#how-to-understand-techdocs-ref-annotation-values + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.8.8 + - @backstage/config@0.1.6 + - @backstage/integration@0.5.9 + +## 0.7.1 + +### Patch Changes + +- 59a5fa319: Migrated files are now printed when `techdocs-cli migrate` is run with the + `--verbose` flag set. +- 54356336e: TechDocs generator stage now supports `mkdocs.yaml` file, in addition to `.yml` + depending on whichever is present at the time of generation. (Assumes the + latest `spotify/techdocs` container, running mkdocs `v1.2.2` or greater). + +## 0.7.0 + +### Minor Changes + +- d32d01e5b: Improve the annotation `backstage.io/techdocs-ref: dir:` that links to a path that is relative to the source of the annotated entity. + This annotation works with the basic and the recommended flow, however, it will be most useful with the basic approach. + + This change remove the deprecation of the `dir` reference and provides first-class support for it. + In addition, this change removes the support of the deprecated `github`, `gitlab`, and `azure/api` locations from the `dir` reference preparer. + + #### Example Usage + + The annotation is convenient if the documentation is stored in the same location, i.e. the same git repository, as the `catalog-info.yaml`. + While it is still supported to add full URLs such as `backstage.io/techdocs-ref: url:https://...` for custom setups, documentation is mostly stored in the same repository as the entity definition. + By automatically resolving the target relative to the registration location of the entity, the configuration overhead for this default setup is minimized. + Since it leverages the `@backstage/integrations` package for the URL resolution, this is compatible with every supported source. + + Consider the following examples: + + 1. "I have a repository with a single `catalog-info.yaml` and a TechDocs page in the root folder!" + + ``` + https://github.com/backstage/example/tree/main/ + |- catalog-info.yaml + | > apiVersion: backstage.io/v1alpha1 + | > kind: Component + | > metadata: + | > name: example + | > annotations: + | > backstage.io/techdocs-ref: dir:. # -> same folder + | > spec: {} + |- docs/ + |- mkdocs.yml + ``` + + 2. "I have a repository with a single `catalog-info.yaml` and my TechDocs page in located in a folder!" + + ``` + https://bitbucket.org/my-owner/my-project/src/master/ + |- catalog-info.yaml + | > apiVersion: backstage.io/v1alpha1 + | > kind: Component + | > metadata: + | > name: example + | > annotations: + | > backstage.io/techdocs-ref: dir:./some-folder # -> subfolder + | > spec: {} + |- some-folder/ + |- docs/ + |- mkdocs.yml + ``` + + 3. "I have a mono repository that hosts multiple components!" + + ``` + https://dev.azure.com/organization/project/_git/repository + |- my-1st-module/ + |- catalog-info.yaml + | > apiVersion: backstage.io/v1alpha1 + | > kind: Component + | > metadata: + | > name: my-1st-module + | > annotations: + | > backstage.io/techdocs-ref: dir:. # -> same folder + | > spec: {} + |- docs/ + |- mkdocs.yml + |- my-2nd-module/ + |- catalog-info.yaml + | > apiVersion: backstage.io/v1alpha1 + | > kind: Component + | > metadata: + | > name: my-2nd-module + | > annotations: + | > backstage.io/techdocs-ref: dir:. # -> same folder + | > spec: {} + |- docs/ + |- mkdocs.yml + |- catalog-info.yaml + | > apiVersion: backstage.io/v1alpha1 + | > kind: Location + | > metadata: + | > name: example + | > spec: + | > targets: + | > - ./*/catalog-info.yaml + ``` + +### Patch Changes + +- 6e5aed1c9: Fix validation of mkdocs.yml docs_dir +- 250984333: Add link to https://backstage.io/docs/features/techdocs/configuration in the log warning message about updating techdocs.generate key. +- Updated dependencies + - @backstage/backend-common@0.8.7 + +## 0.6.8 + +### Patch Changes + +- d5eaab91d: Adds custom docker image support to the techdocs generator. This change adds a new `techdocs.generator` configuration key and deprecates the existing `techdocs.generators.techdocs` key. + + ```yaml + techdocs: + # recommended, going forward: + generator: + runIn: 'docker' # or 'local' + # New optional settings + dockerImage: my-org/techdocs # use a custom docker image + pullImage: false # disable automatic pulling of image (e.g. if custom docker login is required) + # legacy (deprecated): + generators: + techdocs: 'docker' # or 'local' + ``` + +- c18e8eb91: Provide optional `logger: Logger` and `logStream: Writable` arguments to the `GeneratorBase#run(...)` command. + They receive all log messages that are emitted during the generator run. +- ae84b20cf: Revert the upgrade to `fs-extra@10.0.0` as that seemed to have broken all installs inexplicably. +- Updated dependencies + - @backstage/backend-common@0.8.6 + +## 0.6.7 + +### Patch Changes + +- 683308ecf: Fix openStack swift publisher encoding issue. Remove utf8 forced encoding on binary files +- 6841e0113: fix minor version of git-url-parse as 11.5.x introduced a bug for Bitbucket Server +- Updated dependencies + - @backstage/integration@0.5.8 + - @backstage/catalog-model@0.9.0 + - @backstage/backend-common@0.8.5 + +## 0.6.6 + +### Patch Changes + +- ab5cc376f: Use new utilities from `@backstage/backend-common` for safely resolving child paths +- b47fc34bc: Update "service catalog" references to "software catalog" +- Updated dependencies + - @backstage/backend-common@0.8.4 + - @backstage/integration@0.5.7 + +## 0.6.5 + +### Patch Changes + +- c17c0fcf9: Adding additional checks on tech docs to prevent folder traversal via mkdocs.yml docs_dir value. +- Updated dependencies + - @backstage/catalog-model@0.8.4 + +## 0.6.4 + +### Patch Changes + +- aad98c544: Fixes multiple XSS and sanitization bypass vulnerabilities in TechDocs. +- 090594755: Support parsing `mkdocs.yml` files that are using custom yaml tags like + `!!python/name:materialx.emoji.twemoji`. +- Updated dependencies [ebe802bc4] +- Updated dependencies [49d7ec169] + - @backstage/catalog-model@0.8.1 + - @backstage/integration@0.5.5 + +## 0.6.3 + +### Patch Changes + +- 8cefadca0: Adding validation to mkdocs.yml parsing to prevent directory tree traversing +- Updated dependencies [0fd4ea443] +- Updated dependencies [add62a455] +- Updated dependencies [704875e26] + - @backstage/integration@0.5.4 + - @backstage/catalog-model@0.8.0 + +## 0.6.2 + +### Patch Changes + +- 65e6c4541: Remove circular dependencies +- Updated dependencies [f7f7783a3] +- Updated dependencies [c7dad9218] +- Updated dependencies [65e6c4541] +- Updated dependencies [68fdbf014] +- Updated dependencies [5001de908] + - @backstage/catalog-model@0.7.10 + - @backstage/backend-common@0.8.1 + - @backstage/integration@0.5.3 + +## 0.6.1 + +### Patch Changes + +- e04f1ccfb: Fixed a bug that prevented loading static assets from GCS, S3, Azure, and OpenStackSwift whose keys contain spaces or other special characters. +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [f9fb4a205] +- Updated dependencies [16be1d093] + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + +## 0.6.0 + +### Minor Changes + +- e0bfd3d44: Migrate the package to use the `ContainerRunner` interface instead of `runDockerContainer(…)`. + It also no longer provides the `ContainerRunner` as an input to the `GeneratorBase#run(…)` function, but expects it as a constructor parameter instead. + + If you use the `TechdocsGenerator` you need to update the usage: + + ```diff + + const containerRunner = new DockerContainerRunner({ dockerClient }); + + - const generator = new TechdocsGenerator(logger, config); + + const techdocsGenerator = new TechdocsGenerator({ + + logger, + + containerRunner, + + config, + + }); + + await this.generator.run({ + inputDir: preparedDir, + outputDir, + - dockerClient: this.dockerClient, + parsedLocationAnnotation, + etag: newEtag, + }); + ``` + +### Patch Changes + +- e9e56b01a: Adding optional config to enable S3-like API for tech-docs using s3ForcePathStyle option. + This allows providers like LocalStack, Minio and Wasabi (+possibly others) to be used to host tech docs. +- Updated dependencies [e0bfd3d44] +- Updated dependencies [38ca05168] +- Updated dependencies [d8b81fd28] + - @backstage/backend-common@0.7.0 + - @backstage/integration@0.5.2 + - @backstage/catalog-model@0.7.8 + - @backstage/config@0.1.5 + +## 0.5.1 + +### Patch Changes + +- f4af06ebe: Gracefully handle HTTP request failures in download method of AzureBlobStorage publisher. + +## 0.5.0 + +### Minor Changes + +- bc9d62f4f: Move the sanity checks of the publisher configurations to a dedicated `PublisherBase#getReadiness()` method instead of throwing an error when doing `Publisher.fromConfig(...)`. + You should include the check when your backend to get early feedback about a potential misconfiguration: + + ```diff + // packages/backend/src/plugins/techdocs.ts + + export default async function createPlugin({ + logger, + config, + discovery, + reader, + }: PluginEnvironment): Promise { + // ... + + const publisher = await Publisher.fromConfig(config, { + logger, + discovery, + }) + + + // checks if the publisher is working and logs the result + + await publisher.getReadiness(); + + // Docker client (conditionally) used by the generators, based on techdocs.generators config. + const dockerClient = new Docker(); + + // ... + } + ``` + + If you want to crash your application on invalid configurations, you can throw an `Error` to preserve the old behavior. + Please be aware that this is not the recommended for the use in a Backstage backend but might be helpful in CLI tools such as the `techdocs-cli`. + + ```ts + const publisher = await Publisher.fromConfig(config, { + logger, + discovery, + }); + + const ready = await publisher.getReadiness(); + if (!ready.isAvailable) { + throw new Error('Invalid TechDocs publisher configuration'); + } + ``` + +### Patch Changes + +- Updated dependencies [bb5055aee] +- Updated dependencies [5d0740563] + - @backstage/catalog-model@0.7.7 + +## 0.4.5 + +### Patch Changes + +- 8686eb38c: Use errors from `@backstage/errors` +- 424742dc1: Applies only if you use TechDocs local builder instead of building on CI/CD i.e. if `techdocs.builder` in your `app-config.yaml` is set to `'local'` + + Improvements + + 1. Do not check for updates in the repository if a check has been made in the last 60 seconds. This is to prevent the annoying check for update on every page switch or load. + 2. No need to maintain an in-memory etag storage, and use the one stored in `techdocs_metadata.json` file alongside generated docs. + + New feature + + 1. You can now use a mix of basic and recommended setup i.e. `techdocs.builder` is `'local'` but using an external cloud storage instead of local storage. Previously, in this setup, the docs would never get updated. + +- Updated dependencies [8686eb38c] +- Updated dependencies [0434853a5] +- Updated dependencies [8686eb38c] + - @backstage/backend-common@0.6.0 + - @backstage/config@0.1.4 + +## 0.4.4 + +### Patch Changes + +- d7245b733: Remove runDockerContainer, and start using the utility function provided by @backstage/backend-common +- 0b42fff22: Make use of parseLocationReference/stringifyLocationReference +- 2ef5bc7ea: Implement proper AWS Credentials precedence with assume-role and explicit credentials +- aa095e469: OpenStack Swift publisher added for tech-docs. +- bc46435f5: - Improve deprecation warning messaging in logs. + - Replace temp folder path from git provider domain(`source`) to full git host name (`resource`). (See: https://github.com/IonicaBizau/git-url-parse#giturlparseurl) +- a501128db: Refactor log messaging to improve clarity +- ca4a904f6: Add an optional configuration option for setting the url endpoint for AWS S3 publisher: `techdocs.publisher.awsS3.endpoint` +- Updated dependencies [277644e09] +- Updated dependencies [52f613030] +- Updated dependencies [d7245b733] +- Updated dependencies [0b42fff22] +- Updated dependencies [905cbfc96] +- Updated dependencies [761698831] +- Updated dependencies [d4e77ec5f] + - @backstage/integration@0.5.1 + - @backstage/backend-common@0.5.6 + - @backstage/catalog-model@0.7.4 + +## 0.4.3 + +### Patch Changes + +- f43192207: remove usage of res.send() for res.json() and res.end() to ensure content types are more consistently application/json on backend responses and error cases +- 61299519f: Remove read-store-upload loop when uploading S3 objects for TechDocs +- Updated dependencies [12d8f27a6] +- Updated dependencies [497859088] +- Updated dependencies [8adb48df4] + - @backstage/catalog-model@0.7.3 + - @backstage/backend-common@0.5.5 + +## 0.4.2 + +### Patch Changes + +- 2499f6cde: Add support for assuming role in AWS integrations +- 1e4ddd71d: Fix AWS, GCS and Azure publisher to work on Windows. +- Updated dependencies [bad21a085] +- Updated dependencies [a1f5e6545] + - @backstage/catalog-model@0.7.2 + - @backstage/config@0.1.3 + +## 0.4.1 + +### Patch Changes + +- fb28da212: Switched to using `'x-access-token'` for authenticating Git over HTTPS towards GitHub. +- 26e143e60: After TechDocs generate step, insert build timestamp to techdocs_metadata.json +- c6655413d: Improved error reporting in AzureBlobStorage to surface errors when fetching metadata and uploading files fails. +- 44414239f: Pass user and group ID when invoking docker container. When TechDocs invokes Docker, docker could be run as a `root` user which results in generation of files by applications run by non-root user (e.g. TechDocs) will not have access to modify. This PR passes in current user and group ID to docker so that the file permissions of the generated files and folders are correct. +- b0a41c707: Add etag of the prepared file tree to techdocs_metadata.json in the storage +- Updated dependencies [16fb1d03a] +- Updated dependencies [491f3a0ec] +- Updated dependencies [491f3a0ec] +- Updated dependencies [434b4e81a] +- Updated dependencies [fb28da212] + - @backstage/backend-common@0.5.4 + - @backstage/integration@0.5.0 + +## 0.4.0 + +### Minor Changes + +- 08142b256: URL Preparer will now use proper etag based caching introduced in https://github.com/backstage/backstage/pull/4120. Previously, builds used to be cached for 30 minutes. + +### Patch Changes + +- 77ad0003a: Revert AWS SDK version to v2 +- 08142b256: TechDocs will throw warning in backend logs when legacy git preparer or dir preparer is used to preparer docs. Migrate to URL Preparer by updating `backstage.io/techdocs-ref` annotation to be prefixed with `url:`. + Detailed docs are here https://backstage.io/docs/features/techdocs/how-to-guides#how-to-use-url-reader-in-techdocs-prepare-step + See benefits and reason for doing so https://github.com/backstage/backstage/issues/4409 +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [5a5163519] + - @backstage/backend-common@0.5.3 + - @backstage/integration@0.4.0 + +## 0.3.7 + +### Patch Changes + +- c777df180: 1. Added option to use Azure Blob Storage as a choice to store the static generated files for TechDocs. +- e44925723: `techdocs.requestUrl` and `techdocs.storageUrl` are now optional configs and the discovery API will be used to get the URL where techdocs plugin is hosted. +- f0320190d: dir preparer will use URL Reader in its implementation. +- Updated dependencies [c4abcdb60] +- Updated dependencies [2430ee7c2] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [064c513e1] +- Updated dependencies [7881f2117] +- Updated dependencies [3149bfe63] +- Updated dependencies [2e62aea6f] +- Updated dependencies [11cb5ef94] + - @backstage/integration@0.3.2 + - @backstage/backend-common@0.5.2 + - @backstage/catalog-model@0.7.1 + +## 0.3.6 + +### Patch Changes + +- 9dd057662: Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref. +- db2328c88: Add rate limiter for concurrent execution of file uploads in AWS and Google publishers +- Updated dependencies [26a3a6cf0] +- Updated dependencies [664dd08c9] +- Updated dependencies [6800da78d] +- Updated dependencies [9dd057662] + - @backstage/backend-common@0.5.1 + - @backstage/integration@0.3.1 + +## 0.3.5 + +### Patch Changes + +- 53c9c51f2: TechDocs backend now streams files through from Google Cloud Storage to the browser, improving memory usage. +- a5e27d5c1: Create type for TechDocsMetadata (#3716) + + This change introduces a new type (TechDocsMetadata) in packages/techdocs-common. This type is then introduced in the endpoint response in techdocs-backend and in the api interface in techdocs (frontend). + +- Updated dependencies [def2307f3] +- Updated dependencies [0b135e7e0] +- Updated dependencies [294a70cab] +- Updated dependencies [fa8ba330a] +- Updated dependencies [0ea032763] +- Updated dependencies [5345a1f98] +- Updated dependencies [ed6baab66] +- Updated dependencies [09a370426] +- Updated dependencies [a93f42213] + - @backstage/catalog-model@0.7.0 + - @backstage/backend-common@0.5.0 + - @backstage/integration@0.3.0 + +## 0.3.4 + +### Patch Changes + +- a594a7257: @backstage/techdocs-common can now be imported in an environment without @backstage/plugin-techdocs-backend being installed. + +## 0.3.3 + +### Patch Changes + +- 68ad5af51: Improve techdocs-common Generator API for it to be used by techdocs-cli. TechDocs generator.run function now takes + an input AND an output directory. Most probably you use techdocs-common via plugin-techdocs-backend, and so there + is no breaking change for you. + But if you use techdocs-common separately, you need to create an output directory and pass into the generator. +- 371f67ecd: fix to-string breakage of binary files +- f1e74777a: Fix bug where binary files (`png`, etc.) could not load when using AWS or GCS publisher. +- dbe4450c3: Google Cloud authentication in TechDocs has been improved. + + 1. `techdocs.publisher.googleGcs.credentials` is now optional. If it is missing, `GOOGLE_APPLICATION_CREDENTIALS` + environment variable (and some other methods) will be used to authenticate. + Read more here https://cloud.google.com/docs/authentication/production + + 2. `techdocs.publisher.googleGcs.projectId` is no longer used. You can remove it from your `app-config.yaml`. + +- 5826d0973: AWS SDK version bump for TechDocs. +- b3b9445df: AWS S3 authentication in TechDocs has been improved. + + 1. `techdocs.publisher.awsS3.bucketName` is now the only required config. `techdocs.publisher.awsS3.credentials` and `techdocs.publisher.awsS3.region` are optional. + + 2. If `techdocs.publisher.awsS3.credentials` and `techdocs.publisher.awsS3.region` are missing, the AWS environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION` will be used. There are more better ways of setting up AWS authentication. Read the guide at https://backstage.io/docs/features/techdocs/using-cloud-storage + +- Updated dependencies [466354aaa] +- Updated dependencies [f3b064e1c] +- Updated dependencies [abbee6fff] +- Updated dependencies [147fadcb9] + - @backstage/integration@0.2.0 + - @backstage/catalog-model@0.6.1 + - @backstage/backend-common@0.4.3 + +## 0.3.2 + +### Patch Changes + +- 7ec525481: 1. Added option to use AWS S3 as a choice to store the static generated files for TechDocs. +- f8ba88ded: Fix for `integration.github.apiBaseUrl` configuration not properly overriding apiBaseUrl used by techdocs +- 00042e73c: Moving the Git actions to isomorphic-git instead of the node binding version of nodegit +- Updated dependencies [5ecd50f8a] +- Updated dependencies [00042e73c] +- Updated dependencies [0829ff126] +- Updated dependencies [036a84373] + - @backstage/backend-common@0.4.2 + - @backstage/integration@0.1.5 + +## 0.3.1 + +### Patch Changes + +- 8804e8981: Using @backstage/integration package for GitHub/GitLab/Azure tokens and request options. + + Most probably you do not have to make any changes in the app because of this change. + However, if you are using the `DirectoryPreparer` or `CommonGitPreparer` exported by + `@backstage/techdocs-common` package, you now need to add pass in a `config` (from `@backstage/config`) + instance as argument. + + ``` + + const directoryPreparer = new DirectoryPreparer(logger); + const commonGitPreparer = new CommonGitPreparer(logger); + + const directoryPreparer = new DirectoryPreparer(config, logger); + const commonGitPreparer = new CommonGitPreparer(config, logger); + ``` + +## 0.3.0 + +### Minor Changes + +- a8573e53b: techdocs-backend: Simplified file, removing individual preparers and generators. + techdocs-backend: UrlReader is now available to use in preparers. + + In your Backstage app, `packages/backend/plugins/techdocs.ts` file has now been simplified, + to remove registering individual preparers and generators. + + Please update the file when upgrading the version of `@backstage/plugin-techdocs-backend` package. + + ```typescript + const preparers = await Preparers.fromConfig(config, { + logger, + reader, + }); + + const generators = await Generators.fromConfig(config, { + logger, + }); + + const publisher = await Publisher.fromConfig(config, { + logger, + discovery, + }); + ``` + + You should be able to remove unnecessary imports, and just do + + ```typescript + import { + createRouter, + Preparers, + Generators, + Publisher, + } from '@backstage/plugin-techdocs-backend'; + ``` + +## 0.2.0 + +### Minor Changes + +- dae4f3983: _Breaking changes_ + + 1. Added option to use Google Cloud Storage as a choice to store the static generated files for TechDocs. + It can be configured using `techdocs.publisher.type` option in `app-config.yaml`. + Step-by-step guide to configure GCS is available here https://backstage.io/docs/features/techdocs/using-cloud-storage + Set `techdocs.publisher.type` to `'local'` if you want to continue using local filesystem to store TechDocs files. + + 2. `techdocs.builder` is now required and can be set to `'local'` or `'external'`. (Set it to `'local'` for now, since CI/CD build + workflow for TechDocs will be available soon (in few weeks)). + If builder is set to 'local' and you open a TechDocs page, `techdocs-backend` will try to generate the docs, publish to storage and + show the generated docs afterwords. + If builder is set to `'external'`, `techdocs-backend` will only fetch the docs and will NOT try to generate and publish. In this case of `'external'`, + we assume that docs are being built in the CI/CD pipeline of the repository. + TechDocs will not assume a default value for `techdocs.builder`. It is better to explicitly define it in the `app-config.yaml`. + + 3. When configuring TechDocs in your backend, there is a difference in how a new publisher is created. + + ``` + --- const publisher = new LocalPublish(logger, discovery); + +++ const publisher = Publisher.fromConfig(config, logger, discovery); + ``` + + Based on the config `techdocs.publisher.type`, the publisher could be either Local publisher or Google Cloud Storage publisher. + + 4. `techdocs.storageUrl` is now a required config. Should be `http://localhost:7007/api/techdocs/static/docs` in most setups. + + 5. Parts of `@backstage/plugin-techdocs-backend` have been moved to a new package `@backstage/techdocs-common` to generate docs. Also to publish docs + to-and-fro between TechDocs and a storage (either local or external). However, a Backstage app does NOT need to import the `techdocs-common` package - + app should only import `@backstage/plugin-techdocs` and `@backstage/plugin-techdocs-backend`. + + _Patch changes_ + + 1. See all of TechDocs config options and its documentation https://backstage.io/docs/features/techdocs/configuration + + 2. Logic about serving static files and metadata retrieval have been abstracted away from the router in `techdocs-backend` to the instance of publisher. + + 3. Removed Material UI Spinner from TechDocs header. Spinners cause unnecessary UX distraction. + Case 1 (when docs are built and are to be served): Spinners appear for a split second before the name of site shows up. This unnecessarily distracts eyes because spinners increase the size of the Header. A dot (.) would do fine. Definitely more can be done. + Case 2 (when docs are being generated): There is already a linear progress bar (which is recommended in Storybook). + +### Patch Changes + +- Updated dependencies [c911061b7] +- Updated dependencies [1d1c2860f] +- Updated dependencies [0e6298f7e] +- Updated dependencies [4eafdec4a] +- Updated dependencies [ac3560b42] + - @backstage/catalog-model@0.6.0 + - @backstage/backend-common@0.4.1 diff --git a/plugins/techdocs-node/README.md b/plugins/techdocs-node/README.md new file mode 100644 index 0000000000..7bf2e5ba9d --- /dev/null +++ b/plugins/techdocs-node/README.md @@ -0,0 +1,49 @@ +# @backstage/plugin-techdocs-node + +Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli + +This package is used by `techdocs-backend` to serve docs from different types of publishers (Google GCS, Local, etc.). +It is also used to build docs and publish them to storage, by both `techdocs-backend` and `techdocs-cli`. + +## Usage + +Create a preparer instance from the [preparers available](/plugins/techdocs-node/src/stages/prepare) at which takes an Entity instance. +Run the [docs generator](/plugins/techdocs-node/src/stages/generate) on the prepared directory. +Publish the generated directory files to a [storage](/plugins/techdocs-node/src/stages/publish) of your choice. + +Example: + +```js +async () => { + const preparedDir = await preparer.prepare(entity); + + const parsedLocationAnnotation = getLocationForEntity(entity); + const { resultDir } = await generator.run({ + directory: preparedDir, + dockerClient: dockerClient, + parsedLocationAnnotation, + }); + + await publisher.publish({ + entity: entity, + directory: resultDir, + }); +}; +``` + +## Features + +Currently the build process is split up in these three stages. + +- Preparers +- Generators +- Publishers + +Preparers read your entity data and creates a working directory with your documentation source code. For example if you have set your `backstage.io/techdocs-ref` to `url:https://github.com/backstage/backstage.git` it will clone that repository to a temp folder and pass that on to the generator. + +Generators takes the prepared source and runs the `techdocs-container` on it. It then passes on the output folder of that build to the publisher. + +Publishers gets a folder path from the generator and publish it to your storage solution. Read documentation to know more about configuring storage solutions. +http://backstage.io/docs/features/techdocs/configuration + +Any of these can be extended. We want to extend our support to most of the storage providers (Publishers) and source code host providers (Preparers). diff --git a/packages/techdocs-common/__mocks__/@azure/identity.ts b/plugins/techdocs-node/__mocks__/@azure/identity.ts similarity index 100% rename from packages/techdocs-common/__mocks__/@azure/identity.ts rename to plugins/techdocs-node/__mocks__/@azure/identity.ts diff --git a/packages/techdocs-common/__mocks__/@azure/storage-blob.ts b/plugins/techdocs-node/__mocks__/@azure/storage-blob.ts similarity index 100% rename from packages/techdocs-common/__mocks__/@azure/storage-blob.ts rename to plugins/techdocs-node/__mocks__/@azure/storage-blob.ts diff --git a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts b/plugins/techdocs-node/__mocks__/@google-cloud/storage.ts similarity index 100% rename from packages/techdocs-common/__mocks__/@google-cloud/storage.ts rename to plugins/techdocs-node/__mocks__/@google-cloud/storage.ts diff --git a/packages/techdocs-common/__mocks__/@trendyol-js/openstack-swift-sdk.ts b/plugins/techdocs-node/__mocks__/@trendyol-js/openstack-swift-sdk.ts similarity index 100% rename from packages/techdocs-common/__mocks__/@trendyol-js/openstack-swift-sdk.ts rename to plugins/techdocs-node/__mocks__/@trendyol-js/openstack-swift-sdk.ts diff --git a/packages/techdocs-common/__mocks__/aws-sdk.ts b/plugins/techdocs-node/__mocks__/aws-sdk.ts similarity index 100% rename from packages/techdocs-common/__mocks__/aws-sdk.ts rename to plugins/techdocs-node/__mocks__/aws-sdk.ts diff --git a/plugins/techdocs-node/api-report.md b/plugins/techdocs-node/api-report.md new file mode 100644 index 0000000000..4c88ab0fc0 --- /dev/null +++ b/plugins/techdocs-node/api-report.md @@ -0,0 +1,258 @@ +## API Report File for "@backstage/plugin-techdocs-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { CompoundEntityRef } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { ContainerRunner } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import express from 'express'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { Logger } from 'winston'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { UrlReader } from '@backstage/backend-common'; +import { Writable } from 'stream'; + +// @public +export class DirectoryPreparer implements PreparerBase { + // @deprecated + constructor(config: Config, _logger: Logger | null, reader: UrlReader); + static fromConfig( + config: Config, + { logger, reader }: PreparerConfig, + ): DirectoryPreparer; + prepare(entity: Entity, options?: PreparerOptions): Promise; +} + +// @public +export type ETag = string; + +// @public +export type GeneratorBase = { + run(opts: GeneratorRunOptions): Promise; +}; + +// @public +export type GeneratorBuilder = { + register(protocol: SupportedGeneratorKey, generator: GeneratorBase): void; + get(entity: Entity): GeneratorBase; +}; + +// @public +export type GeneratorOptions = { + containerRunner: ContainerRunner; + logger: Logger; +}; + +// @public +export type GeneratorRunOptions = { + inputDir: string; + outputDir: string; + parsedLocationAnnotation?: ParsedLocationAnnotation; + etag?: string; + logger: Logger; + logStream?: Writable; +}; + +// @public +export class Generators implements GeneratorBuilder { + static fromConfig( + config: Config, + options: { + logger: Logger; + containerRunner: ContainerRunner; + }, + ): Promise; + get(entity: Entity): GeneratorBase; + register(generatorKey: SupportedGeneratorKey, generator: GeneratorBase): void; +} + +// @public +export const getDocFilesFromRepository: ( + reader: UrlReader, + entity: Entity, + opts?: + | { + etag?: string | undefined; + logger?: Logger | undefined; + } + | undefined, +) => Promise; + +// @public +export const getLocationForEntity: ( + entity: Entity, + scmIntegration: ScmIntegrationRegistry, +) => ParsedLocationAnnotation; + +// @public +export type MigrateRequest = { + removeOriginal?: boolean; + concurrency?: number; +}; + +// @public +export type ParsedLocationAnnotation = { + type: RemoteProtocol; + target: string; +}; + +// @public +export const parseReferenceAnnotation: ( + annotationName: string, + entity: Entity, +) => ParsedLocationAnnotation; + +// @public +export type PreparerBase = { + prepare(entity: Entity, options?: PreparerOptions): Promise; +}; + +// @public +export type PreparerBuilder = { + register(protocol: RemoteProtocol, preparer: PreparerBase): void; + get(entity: Entity): PreparerBase; +}; + +// @public +export type PreparerConfig = { + logger: Logger; + reader: UrlReader; +}; + +// @public +export type PreparerOptions = { + logger?: Logger; + etag?: ETag; +}; + +// @public +export type PreparerResponse = { + preparedDir: string; + etag: ETag; +}; + +// @public +export class Preparers implements PreparerBuilder { + static fromConfig( + backstageConfig: Config, + { logger, reader }: PreparerConfig, + ): Promise; + get(entity: Entity): PreparerBase; + register(protocol: RemoteProtocol, preparer: PreparerBase): void; +} + +// @public +export class Publisher { + static fromConfig( + config: Config, + { logger, discovery }: PublisherFactory, + ): Promise; +} + +// @public +export interface PublisherBase { + docsRouter(): express.Handler; + fetchTechDocsMetadata( + entityName: CompoundEntityRef, + ): Promise; + getReadiness(): Promise; + hasDocsBeenGenerated(entityName: Entity): Promise; + migrateDocsCase?(migrateRequest: MigrateRequest): Promise; + publish(request: PublishRequest): Promise; +} + +// @public +export type PublisherFactory = { + logger: Logger; + discovery: PluginEndpointDiscovery; +}; + +// @public +export type PublisherType = + | 'local' + | 'googleGcs' + | 'awsS3' + | 'azureBlobStorage' + | 'openStackSwift'; + +// @public +export type PublishRequest = { + entity: Entity; + directory: string; +}; + +// @public +export type PublishResponse = { + remoteUrl?: string; + objects?: string[]; +} | void; + +// @public +export type ReadinessResponse = { + isAvailable: boolean; +}; + +// @public +export type RemoteProtocol = 'url' | 'dir'; + +// @public +export type SupportedGeneratorKey = 'techdocs' | string; + +// @public +export interface TechDocsDocument extends IndexableDocument { + kind: string; + lifecycle: string; + name: string; + namespace: string; + owner: string; + path: string; +} + +// @public +export class TechdocsGenerator implements GeneratorBase { + constructor(options: { + logger: Logger; + containerRunner: ContainerRunner; + config: Config; + scmIntegrations: ScmIntegrationRegistry; + }); + static readonly defaultDockerImage = 'spotify/techdocs:v0.3.7'; + static fromConfig( + config: Config, + options: GeneratorOptions, + ): TechdocsGenerator; + run(options: GeneratorRunOptions): Promise; +} + +// @public +export type TechDocsMetadata = { + site_name: string; + site_description: string; + etag: string; + build_timestamp: number; + files?: string[]; +}; + +// @public +export const transformDirLocation: ( + entity: Entity, + dirAnnotation: ParsedLocationAnnotation, + scmIntegrations: ScmIntegrationRegistry, +) => { + type: 'dir' | 'url'; + target: string; +}; + +// @public +export class UrlPreparer implements PreparerBase { + // @deprecated + constructor(reader: UrlReader, logger: Logger); + static fromConfig({ reader, logger }: PreparerConfig): UrlPreparer; + prepare(entity: Entity, options?: PreparerOptions): Promise; +} +``` diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json new file mode 100644 index 0000000000..745e9145fe --- /dev/null +++ b/plugins/techdocs-node/package.json @@ -0,0 +1,81 @@ +{ + "name": "@backstage/plugin-techdocs-node", + "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", + "version": "0.11.12-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/techdocs-node" + }, + "keywords": [ + "techdocs", + "backstage" + ], + "license": "Apache-2.0", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" + }, + "bugs": { + "url": "https://github.com/backstage/backstage/issues" + }, + "dependencies": { + "@azure/identity": "^2.0.1", + "@azure/storage-blob": "^12.5.0", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-search-common": "^0.3.1-next.0", + "@google-cloud/storage": "^5.6.0", + "@trendyol-js/openstack-swift-sdk": "^0.0.5", + "@types/express": "^4.17.6", + "aws-sdk": "^2.840.0", + "express": "^4.17.1", + "fs-extra": "10.0.1", + "git-url-parse": "^11.6.0", + "js-yaml": "^4.0.0", + "json5": "^2.1.3", + "mime-types": "^2.1.27", + "mock-fs": "^5.1.0", + "p-limit": "^3.1.0", + "recursive-readdir": "^2.2.2", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "^0.15.2-next.0", + "@types/fs-extra": "^9.0.5", + "@types/js-yaml": "^4.0.0", + "@types/mime-types": "^2.1.0", + "@types/mock-fs": "^4.13.0", + "@types/recursive-readdir": "^2.2.0", + "@types/supertest": "^2.0.8", + "supertest": "^6.1.3" + }, + "jest": { + "roots": [ + ".." + ] + } +} diff --git a/packages/techdocs-common/src/helpers.test.ts b/plugins/techdocs-node/src/helpers.test.ts similarity index 100% rename from packages/techdocs-common/src/helpers.test.ts rename to plugins/techdocs-node/src/helpers.test.ts diff --git a/packages/techdocs-common/src/helpers.ts b/plugins/techdocs-node/src/helpers.ts similarity index 100% rename from packages/techdocs-common/src/helpers.ts rename to plugins/techdocs-node/src/helpers.ts diff --git a/plugins/techdocs-node/src/index.ts b/plugins/techdocs-node/src/index.ts new file mode 100644 index 0000000000..a2acf84611 --- /dev/null +++ b/plugins/techdocs-node/src/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli + * + * @packageDocumentation + */ + +export * from './stages'; +export * from './helpers'; +export * from './techdocsTypes'; diff --git a/packages/techdocs-common/src/setupTests.ts b/plugins/techdocs-node/src/setupTests.ts similarity index 100% rename from packages/techdocs-common/src/setupTests.ts rename to plugins/techdocs-node/src/setupTests.ts diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir2.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir2.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir2.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir2.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_valid_doc_dir.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_valid_doc_dir.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_valid_doc_dir.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_valid_doc_dir.yml diff --git a/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml new file mode 100644 index 0000000000..09e8fd7ac7 --- /dev/null +++ b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml @@ -0,0 +1,6 @@ +site_name: Test site name +site_description: Test site description +docs_dir: docs/ +plugins: + - not-techdocs-core + - also-not-techdocs-core diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_comments.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_comments.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_comments.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_comments.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_extensions.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_extensions.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_extensions.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_extensions.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_repo_url.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_repo_url.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_repo_url.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_repo_url.yml diff --git a/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml new file mode 100644 index 0000000000..eea9a8a3d9 --- /dev/null +++ b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml @@ -0,0 +1,5 @@ +site_name: Test site name +site_description: Test site description +# This is a comment that is removed after editing +plugins: + - techdocs-core diff --git a/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml new file mode 100644 index 0000000000..e75b06ada7 --- /dev/null +++ b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml @@ -0,0 +1,3 @@ +site_name: Test site name +site_description: Test site description +docs_dir: docs/ diff --git a/packages/techdocs-common/src/stages/generate/generators.test.ts b/plugins/techdocs-node/src/stages/generate/generators.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/generate/generators.test.ts rename to plugins/techdocs-node/src/stages/generate/generators.test.ts diff --git a/packages/techdocs-common/src/stages/generate/generators.ts b/plugins/techdocs-node/src/stages/generate/generators.ts similarity index 100% rename from packages/techdocs-common/src/stages/generate/generators.ts rename to plugins/techdocs-node/src/stages/generate/generators.ts diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/plugins/techdocs-node/src/stages/generate/helpers.test.ts similarity index 87% rename from packages/techdocs-common/src/stages/generate/helpers.test.ts rename to plugins/techdocs-node/src/stages/generate/helpers.test.ts index 9323ab4f18..7590df94fb 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.test.ts @@ -28,10 +28,14 @@ import { getMkdocsYml, getRepoUrlFromLocationAnnotation, patchIndexPreBuild, - patchMkdocsYmlPreBuild, storeEtagMetadata, validateMkdocsYaml, } from './helpers'; +import { + patchMkdocsYmlPreBuild, + pathMkdocsYmlWithTechdocsPlugin, +} from './mkDocsPatchers'; +import yaml from 'js-yaml'; const mockEntity = { apiVersion: 'version', @@ -65,6 +69,15 @@ const mkdocsYmlWithInvalidDocDir2 = fs.readFileSync( const mkdocsYmlWithComments = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_comments.yml'), ); +const mkdocsYmlWithTechdocsPlugins = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_with_techdocs_plugin.yml'), +); +const mkdocsYmlWithoutPlugins = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_without_plugins.yml'), +); +const mkdocsYmlWithAdditionalPlugins = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_with_additional_plugins.yml'), +); const mockLogger = getVoidLogger(); const warn = jest.spyOn(mockLogger, 'warn'); @@ -289,6 +302,60 @@ describe('helpers', () => { }); }); + describe('pathMkdocsYmlWithTechdocsPlugin', () => { + beforeEach(() => { + mockFs({ + '/mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins, + '/mkdocs_without_plugins.yml': mkdocsYmlWithoutPlugins, + '/mkdocs_with_additional_plugins.yml': mkdocsYmlWithAdditionalPlugins, + }); + }); + it('should not add additional plugins if techdocs exists already in mkdocs file', async () => { + await pathMkdocsYmlWithTechdocsPlugin( + '/mkdocs_with_techdocs_plugin.yml', + mockLogger, + ); + + const updatedMkdocsYml = await fs.readFile( + '/mkdocs_with_techdocs_plugin.yml', + ); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { + plugins: string[]; + }; + expect(parsedYml.plugins).toHaveLength(1); + expect(parsedYml.plugins).toContain('techdocs-core'); + }); + it("should add the needed plugin if it doesn't exist in mkdocs file", async () => { + await pathMkdocsYmlWithTechdocsPlugin( + '/mkdocs_without_plugins.yml', + mockLogger, + ); + + const updatedMkdocsYml = await fs.readFile('/mkdocs_without_plugins.yml'); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { + plugins: string[]; + }; + expect(parsedYml.plugins).toHaveLength(1); + expect(parsedYml.plugins).toContain('techdocs-core'); + }); + it('should not override existing plugins', async () => { + await pathMkdocsYmlWithTechdocsPlugin( + '/mkdocs_with_additional_plugins.yml', + mockLogger, + ); + const updatedMkdocsYml = await fs.readFile( + '/mkdocs_with_additional_plugins.yml', + ); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { + plugins: string[]; + }; + expect(parsedYml.plugins).toHaveLength(3); + expect(parsedYml.plugins).toContain('techdocs-core'); + expect(parsedYml.plugins).toContain('not-techdocs-core'); + expect(parsedYml.plugins).toContain('also-not-techdocs-core'); + }); + }); + describe('patchIndexPreBuild', () => { afterEach(() => { warn.mockClear(); diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/plugins/techdocs-node/src/stages/generate/helpers.ts similarity index 74% rename from packages/techdocs-common/src/stages/generate/helpers.ts rename to plugins/techdocs-node/src/stages/generate/helpers.ts index 2debd5822c..349783d2bd 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.ts @@ -125,7 +125,7 @@ class UnknownTag { constructor(public readonly data: any, public readonly type?: string) {} } -const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([ +export const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([ new Type('', { kind: 'scalar', multi: true, @@ -203,101 +203,6 @@ export const validateMkdocsYaml = async ( return parsedMkdocsYml.docs_dir; }; -/** - * Update the mkdocs.yml file before TechDocs generator uses it to generate docs site. - * - * List of tasks: - * - Add repo_url or edit_uri if it does not exists - * If mkdocs.yml has a repo_url, the generated docs site gets an Edit button on the pages by default. - * If repo_url is missing in mkdocs.yml, we will use techdocs annotation of the entity to possibly get - * the repository URL. - * - * This function will not throw an error since this is not critical to the whole TechDocs pipeline. - * Instead it will log warnings if there are any errors in reading, parsing or writing YAML. - * - * @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site - * @param logger - A logger instance - * @param parsedLocationAnnotation - Object with location url and type - * @param scmIntegrations - the scmIntegration to do url transformations - */ -export const patchMkdocsYmlPreBuild = async ( - mkdocsYmlPath: string, - logger: Logger, - parsedLocationAnnotation: ParsedLocationAnnotation, - scmIntegrations: ScmIntegrationRegistry, -) => { - // We only want to override the mkdocs.yml if it has actually changed. This is relevant if - // used with a 'dir' location on the file system as this would permanently update the file. - let didEdit = false; - - let mkdocsYmlFileString; - try { - mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); - } catch (error) { - assertError(error); - logger.warn( - `Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`, - ); - return; - } - - let mkdocsYml: any; - try { - mkdocsYml = yaml.load(mkdocsYmlFileString, { schema: MKDOCS_SCHEMA }); - - // mkdocsYml should be an object type after successful parsing. - // But based on its type definition, it can also be a string or undefined, which we don't want. - if (typeof mkdocsYml === 'string' || typeof mkdocsYml === 'undefined') { - throw new Error('Bad YAML format.'); - } - } catch (error) { - assertError(error); - logger.warn( - `Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${error.message}`, - ); - return; - } - - // Add edit_uri and/or repo_url to mkdocs.yml if it is missing. - // This will enable the Page edit button generated by MkDocs. - // If the either has been set, keep the original value - if (!('repo_url' in mkdocsYml) && !('edit_uri' in mkdocsYml)) { - const result = getRepoUrlFromLocationAnnotation( - parsedLocationAnnotation, - scmIntegrations, - mkdocsYml.docs_dir, - ); - - if (result.repo_url || result.edit_uri) { - mkdocsYml.repo_url = result.repo_url; - mkdocsYml.edit_uri = result.edit_uri; - didEdit = true; - - logger.info( - `Set ${JSON.stringify( - result, - )}. You can disable this feature by manually setting 'repo_url' or 'edit_uri' according to the MkDocs documentation at https://www.mkdocs.org/user-guide/configuration/#repo_url`, - ); - } - } - - try { - if (didEdit) { - await fs.writeFile( - mkdocsYmlPath, - yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }), - 'utf8', - ); - } - } catch (error) { - assertError(error); - logger.warn( - `Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`, - ); - return; - } -}; - /** * Update docs/index.md file before TechDocs generator uses it to generate docs site, * falling back to docs/README.md or README.md in case a default docs/index.md diff --git a/packages/techdocs-common/src/stages/generate/index.ts b/plugins/techdocs-node/src/stages/generate/index.ts similarity index 100% rename from packages/techdocs-common/src/stages/generate/index.ts rename to plugins/techdocs-node/src/stages/generate/index.ts diff --git a/plugins/techdocs-node/src/stages/generate/mkDocsPatchers.ts b/plugins/techdocs-node/src/stages/generate/mkDocsPatchers.ts new file mode 100644 index 0000000000..d03b5d83c2 --- /dev/null +++ b/plugins/techdocs-node/src/stages/generate/mkDocsPatchers.ts @@ -0,0 +1,166 @@ +/* + * Copyright 2022 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 { Logger } from 'winston'; +import fs from 'fs-extra'; +import yaml from 'js-yaml'; +import { ParsedLocationAnnotation } from '../../helpers'; +import { getRepoUrlFromLocationAnnotation, MKDOCS_SCHEMA } from './helpers'; +import { assertError } from '@backstage/errors'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +type MkDocsObject = { + plugins?: string[]; + docs_dir: string; + repo_url?: string; + edit_uri?: string; +}; + +const patchMkdocsFile = async ( + mkdocsYmlPath: string, + logger: Logger, + updateAction: (mkdocsYml: MkDocsObject) => boolean, +) => { + // We only want to override the mkdocs.yml if it has actually changed. This is relevant if + // used with a 'dir' location on the file system as this would permanently update the file. + let didEdit = false; + + let mkdocsYmlFileString; + try { + mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); + } catch (error) { + assertError(error); + logger.warn( + `Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`, + ); + return; + } + + let mkdocsYml: any; + try { + mkdocsYml = yaml.load(mkdocsYmlFileString, { schema: MKDOCS_SCHEMA }); + + // mkdocsYml should be an object type after successful parsing. + // But based on its type definition, it can also be a string or undefined, which we don't want. + if (typeof mkdocsYml === 'string' || typeof mkdocsYml === 'undefined') { + throw new Error('Bad YAML format.'); + } + } catch (error) { + assertError(error); + logger.warn( + `Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${error.message}`, + ); + return; + } + + didEdit = updateAction(mkdocsYml); + + try { + if (didEdit) { + await fs.writeFile( + mkdocsYmlPath, + yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }), + 'utf8', + ); + } + } catch (error) { + assertError(error); + logger.warn( + `Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`, + ); + return; + } +}; + +/** + * Update the mkdocs.yml file before TechDocs generator uses it to generate docs site. + * + * List of tasks: + * - Add repo_url or edit_uri if it does not exists + * If mkdocs.yml has a repo_url, the generated docs site gets an Edit button on the pages by default. + * If repo_url is missing in mkdocs.yml, we will use techdocs annotation of the entity to possibly get + * the repository URL. + * + * This function will not throw an error since this is not critical to the whole TechDocs pipeline. + * Instead it will log warnings if there are any errors in reading, parsing or writing YAML. + * + * @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site + * @param logger - A logger instance + * @param parsedLocationAnnotation - Object with location url and type + * @param scmIntegrations - the scmIntegration to do url transformations + */ +export const patchMkdocsYmlPreBuild = async ( + mkdocsYmlPath: string, + logger: Logger, + parsedLocationAnnotation: ParsedLocationAnnotation, + scmIntegrations: ScmIntegrationRegistry, +) => { + await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => { + if (!('repo_url' in mkdocsYml) && !('edit_uri' in mkdocsYml)) { + // Add edit_uri and/or repo_url to mkdocs.yml if it is missing. + // This will enable the Page edit button generated by MkDocs. + // If the either has been set, keep the original value + const result = getRepoUrlFromLocationAnnotation( + parsedLocationAnnotation, + scmIntegrations, + mkdocsYml.docs_dir, + ); + + if (result.repo_url || result.edit_uri) { + mkdocsYml.repo_url = result.repo_url; + mkdocsYml.edit_uri = result.edit_uri; + + logger.info( + `Set ${JSON.stringify( + result, + )}. You can disable this feature by manually setting 'repo_url' or 'edit_uri' according to the MkDocs documentation at https://www.mkdocs.org/user-guide/configuration/#repo_url`, + ); + return true; + } + } + return false; + }); +}; + +/** + * Update the mkdocs.yml file before TechDocs generator uses it to generate docs site. + * + * List of tasks: + * - Add techdocs-core plugin to mkdocs file if it doesn't exist + * + * This function will not throw an error since this is not critical to the whole TechDocs pipeline. + * Instead it will log warnings if there are any errors in reading, parsing or writing YAML. + * + * @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site + * @param logger - A logger instance + */ +export const pathMkdocsYmlWithTechdocsPlugin = async ( + mkdocsYmlPath: string, + logger: Logger, +) => { + await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => { + // Modify mkdocs.yaml to contain the needed techdocs-core plugin if it is not there + if (!('plugins' in mkdocsYml)) { + mkdocsYml.plugins = ['techdocs-core']; + return true; + } + + if (mkdocsYml.plugins && !mkdocsYml.plugins.includes('techdocs-core')) { + mkdocsYml.plugins.push('techdocs-core'); + return true; + } + return false; + }); +}; diff --git a/packages/techdocs-common/src/stages/generate/techdocs.test.ts b/plugins/techdocs-node/src/stages/generate/techdocs.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/generate/techdocs.test.ts rename to plugins/techdocs-node/src/stages/generate/techdocs.test.ts diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/plugins/techdocs-node/src/stages/generate/techdocs.ts similarity index 93% rename from packages/techdocs-common/src/stages/generate/techdocs.ts rename to plugins/techdocs-node/src/stages/generate/techdocs.ts index baa2e008a1..fd05c3bf8b 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/plugins/techdocs-node/src/stages/generate/techdocs.ts @@ -26,11 +26,15 @@ import { createOrUpdateMetadata, getMkdocsYml, patchIndexPreBuild, - patchMkdocsYmlPreBuild, runCommand, storeEtagMetadata, validateMkdocsYaml, } from './helpers'; + +import { + patchMkdocsYmlPreBuild, + pathMkdocsYmlWithTechdocsPlugin, +} from './mkDocsPatchers'; import { GeneratorBase, GeneratorConfig, @@ -47,7 +51,7 @@ import { ForwardedError } from '@backstage/errors'; export class TechdocsGenerator implements GeneratorBase { /** * The default docker image (and version) used to generate content. Public - * and static so that techdocs-common consumers can use the same version. + * and static so that techdocs-node consumers can use the same version. */ public static readonly defaultDockerImage = 'spotify/techdocs:v0.3.7'; private readonly logger: Logger; @@ -110,6 +114,10 @@ export class TechdocsGenerator implements GeneratorBase { await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir }); } + if (!this.options.omitTechdocsCoreMkdocsPlugin) { + await pathMkdocsYmlWithTechdocsPlugin(mkdocsYmlPath, childLogger); + } + // Directories to bind on container const mountDirs = { [inputDir]: '/input', @@ -207,5 +215,8 @@ export function readGeneratorConfig( 'docker', dockerImage: config.getOptionalString('techdocs.generator.dockerImage'), pullImage: config.getOptionalBoolean('techdocs.generator.pullImage'), + omitTechdocsCoreMkdocsPlugin: config.getOptionalBoolean( + 'techdocs.generator.mkdocs.omitTechdocsCorePlugin', + ), }; } diff --git a/packages/techdocs-common/src/stages/generate/types.ts b/plugins/techdocs-node/src/stages/generate/types.ts similarity index 98% rename from packages/techdocs-common/src/stages/generate/types.ts rename to plugins/techdocs-node/src/stages/generate/types.ts index 2ff3064991..f46dbdc56a 100644 --- a/packages/techdocs-common/src/stages/generate/types.ts +++ b/plugins/techdocs-node/src/stages/generate/types.ts @@ -39,6 +39,7 @@ export type GeneratorConfig = { runIn: GeneratorRunInType; dockerImage?: string; pullImage?: boolean; + omitTechdocsCoreMkdocsPlugin?: boolean; }; /** diff --git a/packages/techdocs-common/src/stages/index.ts b/plugins/techdocs-node/src/stages/index.ts similarity index 100% rename from packages/techdocs-common/src/stages/index.ts rename to plugins/techdocs-node/src/stages/index.ts diff --git a/packages/techdocs-common/src/stages/prepare/dir.test.ts b/plugins/techdocs-node/src/stages/prepare/dir.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/prepare/dir.test.ts rename to plugins/techdocs-node/src/stages/prepare/dir.test.ts diff --git a/packages/techdocs-common/src/stages/prepare/dir.ts b/plugins/techdocs-node/src/stages/prepare/dir.ts similarity index 100% rename from packages/techdocs-common/src/stages/prepare/dir.ts rename to plugins/techdocs-node/src/stages/prepare/dir.ts diff --git a/packages/techdocs-common/src/stages/prepare/index.ts b/plugins/techdocs-node/src/stages/prepare/index.ts similarity index 100% rename from packages/techdocs-common/src/stages/prepare/index.ts rename to plugins/techdocs-node/src/stages/prepare/index.ts diff --git a/packages/techdocs-common/src/stages/prepare/preparers.ts b/plugins/techdocs-node/src/stages/prepare/preparers.ts similarity index 100% rename from packages/techdocs-common/src/stages/prepare/preparers.ts rename to plugins/techdocs-node/src/stages/prepare/preparers.ts diff --git a/packages/techdocs-common/src/stages/prepare/types.ts b/plugins/techdocs-node/src/stages/prepare/types.ts similarity index 100% rename from packages/techdocs-common/src/stages/prepare/types.ts rename to plugins/techdocs-node/src/stages/prepare/types.ts diff --git a/packages/techdocs-common/src/stages/prepare/url.ts b/plugins/techdocs-node/src/stages/prepare/url.ts similarity index 100% rename from packages/techdocs-common/src/stages/prepare/url.ts rename to plugins/techdocs-node/src/stages/prepare/url.ts diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/plugins/techdocs-node/src/stages/publish/awsS3.test.ts similarity index 99% rename from packages/techdocs-common/src/stages/publish/awsS3.test.ts rename to plugins/techdocs-node/src/stages/publish/awsS3.test.ts index 357df73c8c..326e587a83 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/plugins/techdocs-node/src/stages/publish/awsS3.test.ts @@ -24,7 +24,7 @@ import path from 'path'; import fs from 'fs-extra'; import { AwsS3Publish } from './awsS3'; -// NOTE: /packages/techdocs-common/__mocks__ is being used to mock aws-sdk client library +// NOTE: /plugins/techdocs-node/__mocks__ is being used to mock aws-sdk client library const rootDir = (global as any).rootDir; // Set by setupTests.ts diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/plugins/techdocs-node/src/stages/publish/awsS3.ts similarity index 99% rename from packages/techdocs-common/src/stages/publish/awsS3.ts rename to plugins/techdocs-node/src/stages/publish/awsS3.ts index ed76edc0cb..e5914d2fdf 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/plugins/techdocs-node/src/stages/publish/awsS3.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError, ForwardedError } from '@backstage/errors'; import aws, { Credentials } from 'aws-sdk'; @@ -321,7 +321,7 @@ export class AwsS3Publish implements PublisherBase { } async fetchTechDocsMetadata( - entityName: EntityName, + entityName: CompoundEntityRef, ): Promise { try { return await new Promise(async (resolve, reject) => { diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts similarity index 99% rename from packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts rename to plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts index f1b660555e..3e5e420e4b 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts +++ b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts @@ -24,7 +24,7 @@ import path from 'path'; import fs from 'fs-extra'; import { AzureBlobStoragePublish } from './azureBlobStorage'; -// NOTE: /packages/techdocs-common/__mocks__ is being used to mock Azure client library +// NOTE: /plugins/techdocs-node/__mocks__ is being used to mock Azure client library const rootDir = (global as any).rootDir; // Set by setupTests.ts diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.ts similarity index 99% rename from packages/techdocs-common/src/stages/publish/azureBlobStorage.ts rename to plugins/techdocs-node/src/stages/publish/azureBlobStorage.ts index b082079be2..bcbc10a94d 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.ts @@ -19,7 +19,7 @@ import { ContainerClient, StorageSharedKeyCredential, } from '@azure/storage-blob'; -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError, ForwardedError } from '@backstage/errors'; import express from 'express'; @@ -300,7 +300,7 @@ export class AzureBlobStoragePublish implements PublisherBase { } async fetchTechDocsMetadata( - entityName: EntityName, + entityName: CompoundEntityRef, ): Promise { const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; const entityRootDir = this.legacyPathCasing diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts similarity index 99% rename from packages/techdocs-common/src/stages/publish/googleStorage.test.ts rename to plugins/techdocs-node/src/stages/publish/googleStorage.test.ts index 87a38402d0..6dac092cec 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts +++ b/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts @@ -24,7 +24,7 @@ import path from 'path'; import fs from 'fs-extra'; import { GoogleGCSPublish } from './googleStorage'; -// NOTE: /packages/techdocs-common/__mocks__ is being used to mock Google Cloud Storage client library +// NOTE: /plugins/techdocs-node/__mocks__ is being used to mock Google Cloud Storage client library const rootDir = (global as any).rootDir; // Set by setupTests.ts diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/plugins/techdocs-node/src/stages/publish/googleStorage.ts similarity index 98% rename from packages/techdocs-common/src/stages/publish/googleStorage.ts rename to plugins/techdocs-node/src/stages/publish/googleStorage.ts index c3d9d3330b..987dfc6f09 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/plugins/techdocs-node/src/stages/publish/googleStorage.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError } from '@backstage/errors'; import { File, FileExistsResponse, Storage } from '@google-cloud/storage'; @@ -238,7 +238,9 @@ export class GoogleGCSPublish implements PublisherBase { return { objects }; } - fetchTechDocsMetadata(entityName: EntityName): Promise { + fetchTechDocsMetadata( + entityName: CompoundEntityRef, + ): Promise { return new Promise((resolve, reject) => { const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; const entityDir = this.legacyPathCasing diff --git a/packages/techdocs-common/src/stages/publish/helpers.test.ts b/plugins/techdocs-node/src/stages/publish/helpers.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/helpers.test.ts rename to plugins/techdocs-node/src/stages/publish/helpers.test.ts diff --git a/packages/techdocs-common/src/stages/publish/helpers.ts b/plugins/techdocs-node/src/stages/publish/helpers.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/helpers.ts rename to plugins/techdocs-node/src/stages/publish/helpers.ts diff --git a/packages/techdocs-common/src/stages/publish/index.ts b/plugins/techdocs-node/src/stages/publish/index.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/index.ts rename to plugins/techdocs-node/src/stages/publish/index.ts diff --git a/packages/techdocs-common/src/stages/publish/local.test.ts b/plugins/techdocs-node/src/stages/publish/local.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/local.test.ts rename to plugins/techdocs-node/src/stages/publish/local.test.ts diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/plugins/techdocs-node/src/stages/publish/local.ts similarity index 98% rename from packages/techdocs-common/src/stages/publish/local.ts rename to plugins/techdocs-node/src/stages/publish/local.ts index 9c146d2935..4539d389df 100644 --- a/packages/techdocs-common/src/stages/publish/local.ts +++ b/plugins/techdocs-node/src/stages/publish/local.ts @@ -17,7 +17,7 @@ import { PluginEndpointDiscovery, resolvePackagePath, } from '@backstage/backend-common'; -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import express from 'express'; import fs from 'fs-extra'; @@ -49,7 +49,7 @@ try { ); } catch (err) { // This will most probably never be used. - // The try/catch is introduced so that techdocs-cli can import @backstage/techdocs-common + // The try/catch is introduced so that techdocs-cli can import @backstage/plugin-techdocs-node // on CI/CD without installing techdocs backend plugin. staticDocsDir = os.tmpdir(); } @@ -142,7 +142,7 @@ export class LocalPublish implements PublisherBase { } async fetchTechDocsMetadata( - entityName: EntityName, + entityName: CompoundEntityRef, ): Promise { const metadataPath = this.staticEntityPathJoin( entityName.namespace, diff --git a/packages/techdocs-common/src/stages/publish/migrations/GoogleMigration.ts b/plugins/techdocs-node/src/stages/publish/migrations/GoogleMigration.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/migrations/GoogleMigration.ts rename to plugins/techdocs-node/src/stages/publish/migrations/GoogleMigration.ts diff --git a/packages/techdocs-common/src/stages/publish/migrations/index.ts b/plugins/techdocs-node/src/stages/publish/migrations/index.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/migrations/index.ts rename to plugins/techdocs-node/src/stages/publish/migrations/index.ts diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts b/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts similarity index 98% rename from packages/techdocs-common/src/stages/publish/openStackSwift.test.ts rename to plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts index b240ce73da..aeb4e120f5 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts +++ b/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts @@ -17,7 +17,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { Entity, - EntityName, + CompoundEntityRef, DEFAULT_NAMESPACE, } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; @@ -29,7 +29,7 @@ import path from 'path'; import { OpenStackSwiftPublish } from './openStackSwift'; import { PublisherBase, TechDocsMetadata } from './types'; -// NOTE: /packages/techdocs-common/__mocks__ is being used to mock @trendyol-js/openstack-swift-sdk client library +// NOTE: /plugins/techdocs-node/__mocks__ is being used to mock @trendyol-js/openstack-swift-sdk client library const createMockEntity = (annotations = {}): Entity => { return { @@ -45,7 +45,7 @@ const createMockEntity = (annotations = {}): Entity => { }; }; -const createMockEntityName = (): EntityName => ({ +const createMockEntityName = (): CompoundEntityRef => ({ kind: 'TestKind', name: 'test-component-name', namespace: 'test-namespace', diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/plugins/techdocs-node/src/stages/publish/openStackSwift.ts similarity index 99% rename from packages/techdocs-common/src/stages/publish/openStackSwift.ts rename to plugins/techdocs-node/src/stages/publish/openStackSwift.ts index 62b40f9a76..734a7e4451 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/plugins/techdocs-node/src/stages/publish/openStackSwift.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import express from 'express'; import fs from 'fs-extra'; @@ -194,7 +194,7 @@ export class OpenStackSwiftPublish implements PublisherBase { } async fetchTechDocsMetadata( - entityName: EntityName, + entityName: CompoundEntityRef, ): Promise { return await new Promise(async (resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; diff --git a/packages/techdocs-common/src/stages/publish/publish.test.ts b/plugins/techdocs-node/src/stages/publish/publish.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/publish.test.ts rename to plugins/techdocs-node/src/stages/publish/publish.test.ts diff --git a/packages/techdocs-common/src/stages/publish/publish.ts b/plugins/techdocs-node/src/stages/publish/publish.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/publish.ts rename to plugins/techdocs-node/src/stages/publish/publish.ts diff --git a/packages/techdocs-common/src/stages/publish/types.ts b/plugins/techdocs-node/src/stages/publish/types.ts similarity index 95% rename from packages/techdocs-common/src/stages/publish/types.ts rename to plugins/techdocs-node/src/stages/publish/types.ts index c6ad032484..ff5b9acade 100644 --- a/packages/techdocs-common/src/stages/publish/types.ts +++ b/plugins/techdocs-node/src/stages/publish/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Logger } from 'winston'; import express from 'express'; @@ -133,7 +133,9 @@ export interface PublisherBase { * Retrieve TechDocs Metadata about a site e.g. name, contributors, last updated, etc. * This API uses the techdocs_metadata.json file that co-exists along with the generated docs. */ - fetchTechDocsMetadata(entityName: EntityName): Promise; + fetchTechDocsMetadata( + entityName: CompoundEntityRef, + ): Promise; /** * Route middleware to serve static documentation files for an entity. @@ -151,7 +153,7 @@ export interface PublisherBase { * `techdocs-cli` version `{0.x.y}` and `techdocs-backend` version `{0.x.y}`. * * Implementation of this method is unnecessary in publishers introduced - * after version `{0.x.y}` of `techdocs-common`. + * after version `{0.x.y}` of `techdocs-node`. */ migrateDocsCase?(migrateRequest: MigrateRequest): Promise; } diff --git a/packages/techdocs-common/src/techdocsTypes.ts b/plugins/techdocs-node/src/techdocsTypes.ts similarity index 93% rename from packages/techdocs-common/src/techdocsTypes.ts rename to plugins/techdocs-node/src/techdocsTypes.ts index 0b722f4da5..77436b35d5 100644 --- a/packages/techdocs-common/src/techdocsTypes.ts +++ b/plugins/techdocs-node/src/techdocsTypes.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; /** * TechDocs indexable document interface diff --git a/packages/techdocs-common/src/testUtils/StorageFilesMock.ts b/plugins/techdocs-node/src/testUtils/StorageFilesMock.ts similarity index 100% rename from packages/techdocs-common/src/testUtils/StorageFilesMock.ts rename to plugins/techdocs-node/src/testUtils/StorageFilesMock.ts diff --git a/packages/techdocs-common/src/testUtils/types.ts b/plugins/techdocs-node/src/testUtils/types.ts similarity index 100% rename from packages/techdocs-common/src/testUtils/types.ts rename to plugins/techdocs-node/src/testUtils/types.ts diff --git a/plugins/techdocs/.eslintrc.js b/plugins/techdocs/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/techdocs/.eslintrc.js +++ b/plugins/techdocs/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 97104ea5a4..1f4525f3b6 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,69 @@ # @backstage/plugin-techdocs +## 0.15.1-next.0 + +### Patch Changes + +- 7a1dbe6ce9: The panels of `TechDocsCustomHome` now use the `useEntityOwnership` hook to resolve ownership when the `'ownedByUser'` filter predicate is used. +- Updated dependencies + - @backstage/plugin-catalog@0.10.0-next.0 + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/plugin-search@0.7.3-next.0 + - @backstage/integration-react@0.1.25-next.0 + +## 0.15.0 + +### Minor Changes + +- ee3d6c6f10: **BREAKING:** + Table column utilities `createNameColumn`, `createOwnerColumn`, `createTypeColumn` as well as actions utilities `createCopyDocsUrlAction` and `createStarEntityAction` are no longer directly exported. Instead accessible through DocsTable and EntityListDocsTable. + + Use as following: + + ```tsx + DocsTable.columns.createNameColumn(); + DocsTable.columns.createOwnerColumn(); + DocsTable.columns.createTypeColumn(); + + DocsTable.actions.createCopyDocsUrlAction(); + DocsTable.actions.createStarEntityAction(); + ``` + + - Renamed `DocsResultListItem` to `TechDocsSearchResultListItem`, leaving the old name in place as a deprecations. + + - Renamed `TechDocsPage` to `TechDocsReaderPage`, leaving the old name in place as a deprecations. + + - Renamed `TechDocsPageRenderFunction` to `TechDocsPageRenderFunction`, leaving the old name in place as a deprecations. + + - Renamed `TechDocsPageHeader` to `TechDocsReaderPageHeader`, leaving the old name in place as a deprecations. + + - `LegacyTechDocsHome` marked as deprecated and will be deleted in next release, use `TechDocsCustomHome` instead. + + - `LegacyTechDocsPage` marked as deprecated and will be deleted in next release, use `TechDocsReaderPage` instead. + +### Patch Changes + +- 64b430f80d: chore(deps): bump `react-text-truncate` from 0.17.0 to 0.18.0 +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- c5fda066b1: Collapse techdocs sidebar on small devices +- f590d1681b: Removed usage of deprecated favorite utility methods. +- 5b0f9a75fa: Remove copyright from old footer in documentation generated with previous version of `mkdocs-techdocs-plugin` (`v0.2.2`). +- 0c3ba547a6: Show feedback when copying code snippet to clipboard. +- 0ca964ee0e: Fixed a bug that could cause searches in the in-context TechDocs search bar to show results from a different TechDocs site. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-search@0.7.2 + - @backstage/plugin-catalog@0.9.1 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/integration-react@0.1.24 + ## 0.14.0 ### Minor Changes diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index 917c1d5d85..f7ef6e984a 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -7,11 +7,11 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { CSSProperties } from '@material-ui/styles'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; @@ -89,7 +89,7 @@ export type DocsTableRow = { resolved: { docsUrl: string; ownedByRelationsTitle: string; - ownedByRelations: EntityName[]; + ownedByRelations: CompoundEntityRef[]; }; }; @@ -161,7 +161,7 @@ export const Reader: (props: ReaderProps) => JSX.Element; // @public export type ReaderProps = { - entityRef: EntityName; + entityRef: CompoundEntityRef; withSearch?: boolean; onReady?: () => void; }; @@ -187,9 +187,11 @@ export type TabsConfig = TabConfig[]; export interface TechDocsApi { getApiOrigin(): Promise; // (undocumented) - getEntityMetadata(entityId: EntityName): Promise; + getEntityMetadata( + entityId: CompoundEntityRef, + ): Promise; // (undocumented) - getTechDocsMetadata(entityId: EntityName): Promise; + getTechDocsMetadata(entityId: CompoundEntityRef): Promise; } // @public @@ -208,8 +210,10 @@ export class TechDocsClient implements TechDocsApi { discoveryApi: DiscoveryApi; // (undocumented) getApiOrigin(): Promise; - getEntityMetadata(entityId: EntityName): Promise; - getTechDocsMetadata(entityId: EntityName): Promise; + getEntityMetadata( + entityId: CompoundEntityRef, + ): Promise; + getTechDocsMetadata(entityId: CompoundEntityRef): Promise; } // @public @@ -297,7 +301,7 @@ export const TechDocsReaderPageHeader: ( // @public export type TechDocsReaderPageHeaderProps = PropsWithChildren<{ - entityRef: EntityName; + entityRef: CompoundEntityRef; entityMetadata?: TechDocsEntityMetadata; techDocsMetadata?: TechDocsMetadata; }>; @@ -315,7 +319,7 @@ export type TechDocsReaderPageRenderFunction = ({ }: { techdocsMetadataValue?: TechDocsMetadata | undefined; entityMetadataValue?: TechDocsEntityMetadata | undefined; - entityRef: EntityName; + entityRef: CompoundEntityRef; onReady: () => void; }) => JSX.Element; @@ -324,7 +328,7 @@ export const TechDocsSearch: (props: TechDocsSearchProps) => JSX.Element; // @public export type TechDocsSearchProps = { - entityId: EntityName; + entityId: CompoundEntityRef; debounceTime?: number; }; @@ -348,18 +352,18 @@ export interface TechDocsStorageApi { // (undocumented) getBaseUrl( oldBaseUrl: string, - entityId: EntityName, + entityId: CompoundEntityRef, path: string, ): Promise; // (undocumented) getBuilder(): Promise; // (undocumented) - getEntityDocs(entityId: EntityName, path: string): Promise; + getEntityDocs(entityId: CompoundEntityRef, path: string): Promise; // (undocumented) getStorageUrl(): Promise; // (undocumented) syncEntityDocs( - entityId: EntityName, + entityId: CompoundEntityRef, logHandler?: (line: string) => void, ): Promise; } @@ -384,18 +388,18 @@ export class TechDocsStorageClient implements TechDocsStorageApi { // (undocumented) getBaseUrl( oldBaseUrl: string, - entityId: EntityName, + entityId: CompoundEntityRef, path: string, ): Promise; // (undocumented) getBuilder(): Promise; - getEntityDocs(entityId: EntityName, path: string): Promise; + getEntityDocs(entityId: CompoundEntityRef, path: string): Promise; // (undocumented) getStorageUrl(): Promise; // (undocumented) identityApi: IdentityApi; syncEntityDocs( - entityId: EntityName, + entityId: CompoundEntityRef, logHandler?: (line: string) => void, ): Promise; } diff --git a/plugins/techdocs/dev/index.tsx b/plugins/techdocs/dev/index.tsx index 8dc0e5d30b..d08e914736 100644 --- a/plugins/techdocs/dev/index.tsx +++ b/plugins/techdocs/dev/index.tsx @@ -17,7 +17,7 @@ import { createDevApp } from '@backstage/dev-utils'; import { NotFoundError } from '@backstage/errors'; import React from 'react'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Reader, SyncResult, @@ -82,7 +82,10 @@ function createPage({ }); } - async syncEntityDocs(_: EntityName, logHandler?: (line: string) => void) { + async syncEntityDocs( + _: CompoundEntityRef, + logHandler?: (line: string) => void, + ) { if (syncDocsDelay) { for (let i = 0; i < 10; i++) { setTimeout( diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 42e57f8be8..7da631c6e6 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs", "description": "The Backstage plugin that renders technical documentation for your components", - "version": "0.14.0", + "version": "0.15.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,16 +35,16 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/plugin-search": "^0.7.1", + "@backstage/integration": "^0.8.0", + "@backstage/integration-react": "^0.1.25-next.0", + "@backstage/plugin-catalog": "^0.10.0-next.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", + "@backstage/plugin-search": "^0.7.3-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -65,10 +65,10 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts index 4566bbf028..b349431b0d 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { TechDocsEntityMetadata, TechDocsMetadata } from './types'; import { createApiRef } from '@backstage/core-plugin-api'; @@ -55,14 +55,14 @@ export interface TechDocsStorageApi { getApiOrigin(): Promise; getStorageUrl(): Promise; getBuilder(): Promise; - getEntityDocs(entityId: EntityName, path: string): Promise; + getEntityDocs(entityId: CompoundEntityRef, path: string): Promise; syncEntityDocs( - entityId: EntityName, + entityId: CompoundEntityRef, logHandler?: (line: string) => void, ): Promise; getBaseUrl( oldBaseUrl: string, - entityId: EntityName, + entityId: CompoundEntityRef, path: string, ): Promise; } @@ -77,6 +77,8 @@ export interface TechDocsApi { * Set to techdocs.requestUrl as the URL for techdocs-backend API. */ getApiOrigin(): Promise; - getTechDocsMetadata(entityId: EntityName): Promise; - getEntityMetadata(entityId: EntityName): Promise; + getTechDocsMetadata(entityId: CompoundEntityRef): Promise; + getEntityMetadata( + entityId: CompoundEntityRef, + ): Promise; } diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index eac5da0b6b..f260f00547 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { DiscoveryApi, @@ -62,7 +62,9 @@ export class TechDocsClient implements TechDocsApi { * * @param entityId - Object containing entity data like name, namespace, etc. */ - async getTechDocsMetadata(entityId: EntityName): Promise { + async getTechDocsMetadata( + entityId: CompoundEntityRef, + ): Promise { const { kind, namespace, name } = entityId; const apiOrigin = await this.getApiOrigin(); @@ -84,7 +86,7 @@ export class TechDocsClient implements TechDocsApi { * @param entityId - Object containing entity data like name, namespace, etc. */ async getEntityMetadata( - entityId: EntityName, + entityId: CompoundEntityRef, ): Promise { const { kind, namespace, name } = entityId; @@ -149,7 +151,10 @@ export class TechDocsStorageClient implements TechDocsStorageApi { * @returns HTML content of the docs page as string * @throws Throws error when the page is not found. */ - async getEntityDocs(entityId: EntityName, path: string): Promise { + async getEntityDocs( + entityId: CompoundEntityRef, + path: string, + ): Promise { const { kind, namespace, name } = entityId; const storageUrl = await this.getStorageUrl(); @@ -190,7 +195,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi { * @throws Throws error on error from sync endpoint in Techdocs Backend */ async syncEntityDocs( - entityId: EntityName, + entityId: CompoundEntityRef, logHandler: (line: string) => void = () => {}, ): Promise { const { kind, namespace, name } = entityId; @@ -243,7 +248,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi { async getBaseUrl( oldBaseUrl: string, - entityId: EntityName, + entityId: CompoundEntityRef, path: string, ): Promise { const { kind, namespace, name } = entityId; diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx index 8618c4432f..48c1fa248c 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx @@ -23,8 +23,8 @@ import { import { CatalogApi, catalogApiRef, - DefaultStarredEntitiesApi, starredEntitiesApiRef, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { MockStorageApi, @@ -36,16 +36,8 @@ import React from 'react'; import { rootDocsRouteRef } from '../../routes'; import { DefaultTechDocsHome } from './DefaultTechDocsHome'; -jest.mock('@backstage/plugin-catalog-react', () => { - const actual = jest.requireActual('@backstage/plugin-catalog-react'); - return { - ...actual, - useOwnUser: () => 'test-user', - }; -}); - const mockCatalogApi = { - getEntityByName: () => Promise.resolve(), + getEntityByRef: () => Promise.resolve(), getEntities: async () => ({ items: [ { @@ -73,7 +65,7 @@ describe('TechDocs Home', () => { [catalogApiRef, mockCatalogApi], [configApiRef, configApi], [storageApiRef, storageApi], - [starredEntitiesApiRef, new DefaultStarredEntitiesApi({ storageApi })], + [starredEntitiesApiRef, new MockStarredEntitiesApi()], ); it('should render a TechDocs home page', async () => { diff --git a/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx index a8fd472f7c..4c33f69fb7 100644 --- a/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx @@ -24,16 +24,8 @@ import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { ConfigApi, configApiRef } from '@backstage/core-plugin-api'; import { rootDocsRouteRef } from '../../routes'; -jest.mock('@backstage/plugin-catalog-react', () => { - const actual = jest.requireActual('@backstage/plugin-catalog-react'); - return { - ...actual, - useOwnUser: () => 'test-user', - }; -}); - const mockCatalogApi = { - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), getEntities: async () => ({ items: [ { diff --git a/plugins/techdocs/src/home/components/Tables/DocsTable.tsx b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx index b875ba4e98..883c496191 100644 --- a/plugins/techdocs/src/home/components/Tables/DocsTable.tsx +++ b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx @@ -20,7 +20,7 @@ import useCopyToClipboard from 'react-use/lib/useCopyToClipboard'; import { useRouteRef, useApi, configApiRef } from '@backstage/core-plugin-api'; import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { - formatEntityRefTitle, + humanizeEntityRef, getEntityRelations, } from '@backstage/plugin-catalog-react'; import { rootDocsRouteRef } from '../../../routes'; @@ -76,7 +76,7 @@ export const DocsTable = (props: DocsTableProps) => { }), ownedByRelations, ownedByRelationsTitle: ownedByRelations - .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) + .map(r => humanizeEntityRef(r, { defaultKind: 'group' })) .join(', '), }, }; diff --git a/plugins/techdocs/src/home/components/Tables/actions.tsx b/plugins/techdocs/src/home/components/Tables/actions.tsx index d0dbb8bf47..3fd8881718 100644 --- a/plugins/techdocs/src/home/components/Tables/actions.tsx +++ b/plugins/techdocs/src/home/components/Tables/actions.tsx @@ -16,11 +16,16 @@ import React from 'react'; import ShareIcon from '@material-ui/icons/Share'; -import { - favoriteEntityIcon, - favoriteEntityTooltip, -} from '@backstage/plugin-catalog-react'; import { DocsTableRow } from './types'; +import { withStyles } from '@material-ui/styles'; +import Star from '@material-ui/icons/Star'; +import StarBorder from '@material-ui/icons/StarBorder'; + +const YellowStar = withStyles({ + root: { + color: '#f3ba37', + }, +})(Star); /** * Not directly exported, but through DocsTable.actions and EntityListDocsTable.actions @@ -46,8 +51,8 @@ export const actionFactories = { const isStarred = isStarredEntity(entity); return { cellStyle: { paddingLeft: '1em' }, - icon: () => favoriteEntityIcon(isStarred), - tooltip: favoriteEntityTooltip(isStarred), + icon: () => (isStarred ? : ), + tooltip: isStarred ? 'Remove from favorites' : 'Add to favorites', onClick: () => toggleStarredEntity(entity), }; }; diff --git a/plugins/techdocs/src/home/components/Tables/types.ts b/plugins/techdocs/src/home/components/Tables/types.ts index c076d4c12d..6c06526d07 100644 --- a/plugins/techdocs/src/home/components/Tables/types.ts +++ b/plugins/techdocs/src/home/components/Tables/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; /** * Generic representing the metadata structure for a docs table row. @@ -26,6 +26,6 @@ export type DocsTableRow = { resolved: { docsUrl: string; ownedByRelationsTitle: string; - ownedByRelations: EntityName[]; + ownedByRelations: CompoundEntityRef[]; }; }; diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx index e9c3a55b15..6f09a70b5e 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx @@ -22,16 +22,8 @@ import { TechDocsCustomHome, PanelType } from './TechDocsCustomHome'; import { ApiProvider } from '@backstage/core-app-api'; import { rootDocsRouteRef } from '../../routes'; -jest.mock('@backstage/plugin-catalog-react', () => { - const actual = jest.requireActual('@backstage/plugin-catalog-react'); - return { - ...actual, - useOwnUser: () => 'test-user', - }; -}); - const mockCatalogApi = { - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), getEntities: async () => ({ items: [ { diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx index c8149f1ff2..7023560e83 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx @@ -15,21 +15,16 @@ */ import React, { useState } from 'react'; -import useAsync, { AsyncState } from 'react-use/lib/useAsync'; +import useAsync from 'react-use/lib/useAsync'; import { makeStyles } from '@material-ui/core'; import { CSSProperties } from '@material-ui/styles'; import { CATALOG_FILTER_EXISTS, catalogApiRef, CatalogApi, - isOwnerOf, + useEntityOwnership, } from '@backstage/plugin-catalog-react'; -import { - DEFAULT_NAMESPACE, - Entity, - parseEntityRef, - UserEntity, -} from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { DocsTable } from './Tables'; import { DocsCardGrid } from './Grids'; import { TechDocsPageWrapper } from './TechDocsPageWrapper'; @@ -43,8 +38,7 @@ import { SupportButton, ContentHeader, } from '@backstage/core-components'; - -import { identityApiRef, useApi } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; const panels = { DocsTable: DocsTable, @@ -104,16 +98,16 @@ const CustomPanel = ({ }, }); const classes = useStyles(); - const { value: user } = useOwnUser(); + const { loading: loadingOwnership, isOwnedEntity } = useEntityOwnership(); const Panel = panels[config.panelType]; const shownEntities = entities.filter(entity => { if (config.filterPredicate === 'ownedByUser') { - if (!user) { + if (loadingOwnership) { return false; } - return isOwnerOf(user, entity); + return isOwnedEntity(entity); } return ( @@ -225,18 +219,3 @@ export const TechDocsCustomHome = (props: TechDocsCustomHomeProps) => { ); }; - -function useOwnUser(): AsyncState { - const catalogApi = useApi(catalogApiRef); - const identityApi = useApi(identityApiRef); - - return useAsync(async () => { - const identity = await identityApi.getBackstageIdentity(); - return catalogApi.getEntityByName( - parseEntityRef(identity.userEntityRef, { - defaultKind: 'User', - defaultNamespace: DEFAULT_NAMESPACE, - }), - ) as Promise; - }, [catalogApi, identityApi]); -} diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 4042585b6d..6aa5c99be4 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -34,7 +34,7 @@ import { alpha, } from '@material-ui/core'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { useApi, configApiRef } from '@backstage/core-plugin-api'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { BackstageTheme } from '@backstage/theme'; @@ -68,7 +68,7 @@ import { useReaderState } from './useReaderState'; * @public */ export type ReaderProps = { - entityRef: EntityName; + entityRef: CompoundEntityRef; withSearch?: boolean; onReady?: () => void; }; @@ -95,7 +95,7 @@ const TechDocsReaderContext = createContext( const TechDocsReaderProvider = ({ children, entityRef, -}: PropsWithChildren<{ entityRef: EntityName }>) => { +}: PropsWithChildren<{ entityRef: CompoundEntityRef }>) => { const { '*': path } = useParams(); const { kind, namespace, name } = entityRef; const value = useReaderState(kind, namespace, name, path); @@ -116,7 +116,7 @@ const TechDocsReaderProvider = ({ * @internal */ export const withTechDocsReaderProvider = - (Component: ComponentType, entityRef: EntityName) => + (Component: ComponentType, entityRef: CompoundEntityRef) => (props: T) => ( @@ -157,7 +157,9 @@ const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; * todo: Make public or stop exporting (see others: "altReaderExperiments") * @internal */ -export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { +export const useTechDocsReaderDom = ( + entityRef: CompoundEntityRef, +): Element | null => { const navigate = useNavigate(); const theme = useTheme(); const techdocsStorageApi = useApi(techdocsStorageApiRef); @@ -322,7 +324,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { --md-source-version-icon: url('data:image/svg+xml;charset=utf-8,'); --md-version-icon: url('data:image/svg+xml;charset=utf-8,'); } - + :host > * { /* CODE */ --md-code-fg-color: ${theme.palette.text.primary}; @@ -438,7 +440,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { .md-main__inner { margin-top: 0; } - + .md-sidebar { height: calc(100% - 100px); position: fixed; @@ -450,13 +452,13 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { .md-sidebar--secondary { right: ${theme.spacing(3)}px; } - + .md-content { max-width: calc(100% - 16rem * 2); margin-left: 16rem; margin-bottom: 50px; } - + .md-footer { position: fixed; bottom: 0px; @@ -471,7 +473,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { .md-dialog { background-color: unset; } - + @media screen and (max-width: 76.1875em) { .md-nav { transition: none !important; @@ -567,7 +569,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { }), injectCss({ // Typeset - css: ` + css: ` .md-typeset { font-size: var(--md-typeset-font-size); } @@ -600,11 +602,11 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { .md-typeset .md-content__button { color: var(--md-default-fg-color); } - + .md-typeset hr { border-bottom: 0.05rem dotted ${theme.palette.divider}; } - + .md-typeset details { font-size: var(--md-typeset-font-size) !important; } @@ -621,7 +623,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { .md-typeset details[open] > summary:after { transform: rotate(90deg) translateX(-50%) !important; } - + .md-typeset blockquote { color: var(--md-default-fg-color--light); border-left: 0.2rem solid var(--md-default-fg-color--light); @@ -667,13 +669,13 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { .highlight .md-clipboard:after { content: unset; } - + .highlight .nx { color: ${isDarkTheme ? '#ff53a3' : '#ec407a'}; } /* CODE HILITE */ - .codehilite .gd { + .codehilite .gd { background-color: ${ isDarkTheme ? 'rgba(248,81,73,0.65)' : '#fdd' }; diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx index d03ea2ef61..184ab24b6d 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx @@ -21,12 +21,12 @@ import useAsync from 'react-use/lib/useAsync'; import { techdocsApiRef } from '../../api'; import { LegacyTechDocsPage } from './LegacyTechDocsPage'; import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { useApi, useApp } from '@backstage/core-plugin-api'; import { Page } from '@backstage/core-components'; /** - * Helper function that gives the children of {@link TechDocsReaderPage} acccess to techdocs and entity metadata + * Helper function that gives the children of {@link TechDocsReaderPage} access to techdocs and entity metadata * * @public */ @@ -37,7 +37,7 @@ export type TechDocsReaderPageRenderFunction = ({ }: { techdocsMetadataValue?: TechDocsMetadata | undefined; entityMetadataValue?: TechDocsEntityMetadata | undefined; - entityRef: EntityName; + entityRef: CompoundEntityRef; onReady: () => void; }) => JSX.Element; diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader.tsx index 928833efed..5867641c48 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader.tsx @@ -19,7 +19,7 @@ import CodeIcon from '@material-ui/icons/Code'; import { useRouteRef } from '@backstage/core-plugin-api'; import { Header, HeaderLabel } from '@backstage/core-components'; -import { EntityName, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { CompoundEntityRef, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { EntityRefLink, EntityRefLinks, @@ -35,7 +35,7 @@ import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types'; * @public */ export type TechDocsReaderPageHeaderProps = PropsWithChildren<{ - entityRef: EntityName; + entityRef: CompoundEntityRef; entityMetadata?: TechDocsEntityMetadata; techDocsMetadata?: TechDocsMetadata; }>; diff --git a/plugins/techdocs/src/reader/components/useRawPage.ts b/plugins/techdocs/src/reader/components/useRawPage.ts index 61b04a5a82..311f8554a9 100644 --- a/plugins/techdocs/src/reader/components/useRawPage.ts +++ b/plugins/techdocs/src/reader/components/useRawPage.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import useAsyncRetry from 'react-use/lib/useAsyncRetry'; import { AsyncState } from 'react-use/lib/useAsync'; import { techdocsStorageApiRef } from '../../api'; @@ -22,7 +22,7 @@ import { useApi } from '@backstage/core-plugin-api'; export type RawPage = { content: string; path: string; - entityId: EntityName; + entityId: CompoundEntityRef; }; export function useRawPage( diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts index f3906b7b91..2e3b853e42 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { TechDocsStorageApi } from '../../api'; import type { Transformer } from './transformer'; type AddBaseUrlOptions = { techdocsStorageApi: TechDocsStorageApi; - entityId: EntityName; + entityId: CompoundEntityRef; path: string; }; diff --git a/plugins/techdocs/src/search/components/TechDocsSearch.tsx b/plugins/techdocs/src/search/components/TechDocsSearch.tsx index c817fb3217..30cc692b7c 100644 --- a/plugins/techdocs/src/search/components/TechDocsSearch.tsx +++ b/plugins/techdocs/src/search/components/TechDocsSearch.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { SearchContextProvider, useSearch } from '@backstage/plugin-search'; import { makeStyles, @@ -42,7 +42,7 @@ const useStyles = makeStyles({ * @public */ export type TechDocsSearchProps = { - entityId: EntityName; + entityId: CompoundEntityRef; debounceTime?: number; }; diff --git a/plugins/todo-backend/.eslintrc.js b/plugins/todo-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/todo-backend/.eslintrc.js +++ b/plugins/todo-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/todo-backend/CHANGELOG.md b/plugins/todo-backend/CHANGELOG.md index 28ed332560..c4b2904df6 100644 --- a/plugins/todo-backend/CHANGELOG.md +++ b/plugins/todo-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-todo-backend +## 0.1.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/catalog-client@0.9.0-next.0 + +## 0.1.25 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + - @backstage/integration@0.8.0 + ## 0.1.24 ### Patch Changes diff --git a/plugins/todo-backend/api-report.md b/plugins/todo-backend/api-report.md index 71094e03f8..277c87fea9 100644 --- a/plugins/todo-backend/api-report.md +++ b/plugins/todo-backend/api-report.md @@ -4,10 +4,10 @@ ```ts import { CatalogApi } from '@backstage/catalog-client'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { EntityName } from '@backstage/catalog-model'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { ScmIntegrations } from '@backstage/integration'; import { UrlReader } from '@backstage/backend-common'; @@ -19,7 +19,7 @@ export function createTodoParser(options?: TodoParserOptions): TodoParser; // @public (undocumented) export type ListTodosRequest = { - entity?: EntityName; + entity?: CompoundEntityRef; offset?: number; limit?: number; orderBy?: { @@ -127,7 +127,7 @@ export class TodoScmReader implements TodoReader { // @public (undocumented) export type TodoScmReaderOptions = { - logger: Logger_2; + logger: Logger; reader: UrlReader; integrations: ScmIntegrations; parser?: TodoParser; diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 21f914c04a..3f69a8ca7e 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo-backend", "description": "A Backstage backend plugin that lets you browse TODO comments in your source code", - "version": "0.1.24", + "version": "0.1.26-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,12 +29,12 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.13.0-next.0", + "@backstage/catalog-client": "^0.9.0-next.0", + "@backstage/catalog-model": "^0.13.0-next.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", + "@backstage/integration": "^0.8.0", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -43,7 +43,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.2-next.0", "@types/supertest": "^2.0.8", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/todo-backend/src/service/TodoReaderService.test.ts b/plugins/todo-backend/src/service/TodoReaderService.test.ts index 985687c6bb..943462bd63 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.test.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.test.ts @@ -44,6 +44,7 @@ function mockCatalogClient(entity?: Entity): jest.Mocked { const mock = { addLocation: jest.fn(), getEntities: jest.fn(), + getEntityByRef: jest.fn(), getEntityByName: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), @@ -54,7 +55,7 @@ function mockCatalogClient(entity?: Entity): jest.Mocked { getEntityFacets: jest.fn(), }; if (entity) { - mock.getEntityByName.mockReturnValue(entity); + mock.getEntityByRef.mockReturnValue(entity); } return mock; } @@ -93,7 +94,7 @@ describe('TodoReaderService', () => { offset: 0, limit: 10, }); - expect(catalogClient.getEntityByName).toHaveBeenCalledWith(entityName, { + expect(catalogClient.getEntityByRef).toHaveBeenCalledWith(entityName, { token: undefined, }); }); @@ -304,7 +305,7 @@ describe('TodoReaderService', () => { message: 'Entity not found, component:default/my-component', }), ); - expect(catalogClient.getEntityByName).toHaveBeenCalledWith(entityName, { + expect(catalogClient.getEntityByRef).toHaveBeenCalledWith(entityName, { token: undefined, }); }); diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts index 86f53057da..4397f34045 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -66,7 +66,7 @@ export class TodoReaderService implements TodoService { throw new InputError('Entity filter is required to list TODOs'); } const token = options?.token; - const entity = await this.catalogClient.getEntityByName(req.entity, { + const entity = await this.catalogClient.getEntityByRef(req.entity, { token, }); if (!entity) { diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts index a0e6a6ec94..33356a83d6 100644 --- a/plugins/todo-backend/src/service/router.ts +++ b/plugins/todo-backend/src/service/router.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName, parseEntityRef } from '@backstage/catalog-model'; +import { CompoundEntityRef, parseEntityRef } from '@backstage/catalog-model'; import { InputError } from '@backstage/errors'; import express from 'express'; import Router from 'express-promise-router'; @@ -52,7 +52,7 @@ export async function createRouter( if (entityRef && typeof entityRef !== 'string') { throw new InputError(`entity query must be a string`); } - let entity: EntityName | undefined = undefined; + let entity: CompoundEntityRef | undefined = undefined; if (entityRef) { try { entity = parseEntityRef(entityRef); diff --git a/plugins/todo-backend/src/service/types.ts b/plugins/todo-backend/src/service/types.ts index 90ee6bc913..88d2f2b340 100644 --- a/plugins/todo-backend/src/service/types.ts +++ b/plugins/todo-backend/src/service/types.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { TodoItem } from '../lib'; /** @public */ export type ListTodosRequest = { - entity?: EntityName; + entity?: CompoundEntityRef; offset?: number; limit?: number; orderBy?: { diff --git a/plugins/todo/.eslintrc.js b/plugins/todo/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/todo/.eslintrc.js +++ b/plugins/todo/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/todo/CHANGELOG.md b/plugins/todo/CHANGELOG.md index 3a5d1811fe..07f88b928b 100644 --- a/plugins/todo/CHANGELOG.md +++ b/plugins/todo/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-todo +## 0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.2.2 ### Patch Changes diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 11ce818672..e1dfd3334a 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo", "description": "A Backstage plugin that lets you browse TODO comments in your source code", - "version": "0.2.2", + "version": "0.2.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.13.0-next.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.9.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -45,10 +45,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/todo/src/plugin.test.tsx b/plugins/todo/src/plugin.test.tsx index 57b9be291d..af4704c16f 100644 --- a/plugins/todo/src/plugin.test.tsx +++ b/plugins/todo/src/plugin.test.tsx @@ -19,6 +19,7 @@ import { Route } from 'react-router'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { todoPlugin, EntityTodoContent } from './plugin'; import { todoApiRef } from './api'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; describe('todo', () => { it('should export plugin', () => { @@ -47,7 +48,15 @@ describe('todo', () => { ], ]} > - } /> + + } /> + , ); diff --git a/plugins/user-settings/.eslintrc.js b/plugins/user-settings/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/user-settings/.eslintrc.js +++ b/plugins/user-settings/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 49a5a5d51b..f5f58798f6 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-user-settings +## 0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.1-next.0 + +## 0.4.0 + +### Minor Changes + +- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.21 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index d9777bda83..ce499e4d58 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-user-settings", "description": "A Backstage plugin that provides a settings page", - "version": "0.3.21", + "version": "0.4.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,8 +34,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,10 +47,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx index 8cfb3a7257..7025167658 100644 --- a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx +++ b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx @@ -17,11 +17,9 @@ import Star from '@material-ui/icons/Star'; import React from 'react'; import { ProviderSettingsItem } from './ProviderSettingsItem'; import { - auth0AuthApiRef, githubAuthApiRef, gitlabAuthApiRef, googleAuthApiRef, - oauth2ApiRef, oktaAuthApiRef, microsoftAuthApiRef, bitbucketAuthApiRef, @@ -66,14 +64,6 @@ export const DefaultProviderSettings = ({ configuredProviders }: Props) => ( icon={Star} /> )} - {configuredProviders.includes('auth0') && ( - - )} {configuredProviders.includes('okta') && ( ( icon={Star} /> )} - {configuredProviders.includes('oauth2') && ( - - )} ); diff --git a/plugins/xcmetrics/.eslintrc.js b/plugins/xcmetrics/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/xcmetrics/.eslintrc.js +++ b/plugins/xcmetrics/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/xcmetrics/CHANGELOG.md b/plugins/xcmetrics/CHANGELOG.md index 4ceecc6c7a..82e4f409fb 100644 --- a/plugins/xcmetrics/CHANGELOG.md +++ b/plugins/xcmetrics/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-xcmetrics +## 0.2.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.1-next.0 + +## 0.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.2.20 ### Patch Changes diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 623b27e4ab..599f2e0ea7 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-xcmetrics", "description": "A Backstage plugin that shows XCode build metrics for your components", - "version": "0.2.20", + "version": "0.2.22-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,8 +24,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.1-next.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", @@ -40,10 +40,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.2-next.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.25-next.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index 6635dd8b22..a39cf700f5 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -213,6 +213,7 @@ const NO_WARNING_PACKAGES = [ 'packages/errors', 'packages/integration', 'packages/integration-react', + 'packages/search-common', 'packages/techdocs-common', 'packages/test-utils', 'packages/theme', @@ -222,11 +223,15 @@ const NO_WARNING_PACKAGES = [ 'plugins/auth-node', 'plugins/catalog-backend', 'plugins/catalog-backend-module-aws', + 'plugins/catalog-backend-module-azure', + 'plugins/catalog-backend-module-gitlab', 'plugins/catalog-backend-module-ldap', 'plugins/catalog-backend-module-msgraph', 'plugins/catalog-common', 'plugins/catalog-graph', 'plugins/catalog-react', + 'plugins/periskop', + 'plugins/periskop-backend', 'plugins/permission-backend', 'plugins/permission-common', 'plugins/permission-node', @@ -235,7 +240,10 @@ const NO_WARNING_PACKAGES = [ 'plugins/scaffolder-backend-module-rails', 'plugins/scaffolder-backend-module-yeoman', 'plugins/scaffolder-common', + 'plugins/search-backend-node', + 'plugins/search-common', 'plugins/techdocs-backend', + 'plugins/techdocs-node', 'plugins/tech-insights', 'plugins/tech-insights-backend', 'plugins/tech-insights-backend-module-jsonfc', diff --git a/scripts/list-deprecations.js b/scripts/list-deprecations.js index 9a159927fc..8047158138 100755 --- a/scripts/list-deprecations.js +++ b/scripts/list-deprecations.js @@ -57,9 +57,7 @@ class ReleaseProvider { ); // Filter out just the releases - const releases = tagOutput - .split('\n') - .filter(l => l.startsWith('release-')); + const releases = tagOutput.split('\n').filter(l => l.startsWith('v')); // Then find the earliest release that affected our package for (const release of releases) { diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index be82e19344..c4a790b2cc 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -28,6 +28,10 @@ const execFile = promisify(execFileCb); // All of these are considered to be main-line release branches const MAIN_BRANCHES = ['master', 'origin/master', 'changeset-release/master']; +// This prefix is used for patch branches, followed by the release version +// For example, `patch/v1.2.0` +const PATCH_BRANCH_PREFIX = 'patch/v'; + const DEPENDENCY_TYPES = [ 'dependencies', 'devDependencies', @@ -35,25 +39,79 @@ const DEPENDENCY_TYPES = [ 'peerDependencies', ]; +/** + * Finds the tip of the patch branch of a given release version. + * Returns undefined if no patch branch exists. + */ +async function findTipOfPatchBranch(repo, release) { + try { + await execFile('git', ['fetch', 'origin', PATCH_BRANCH_PREFIX + release], { + shell: true, + cwd: repo.root.dir, + }); + } catch (error) { + if (error.stderr?.match(/fatal: couldn't find remote ref/i)) { + return undefined; + } + throw error; + } + const { stdout: refStr } = await execFile('git', ['rev-parse', 'FETCH_HEAD']); + return refStr.trim(); +} + +/** + * Returns a map of packages to their versions for any package version + * in that does not match the current version in the working directory. + */ +async function detectPatchVersionsForRef(repo, ref) { + const patchVersions = new Map(); + + for (const pkg of repo.packages) { + const pkgJsonPath = path.join( + path.relative(repo.root.dir, pkg.dir), + 'package.json', + ); + try { + const { stdout: pkgJsonStr } = await execFile('git', [ + 'show', + `${ref}:${pkgJsonPath}`, + ]); + if (pkgJsonStr) { + const releasePkgJson = JSON.parse(pkgJsonStr); + const pkgJson = pkg.packageJson; + if (releasePkgJson.name !== pkgJson.name) { + throw new Error( + `Mismatched package name at ${pkg.dir}, ${releasePkgJson.name} !== ${pkgJson.name}`, + ); + } + if (releasePkgJson.version !== pkgJson.version) { + patchVersions.set(pkgJson.name, releasePkgJson.version); + } + } + } catch (error) { + if ( + error.stderr?.match(/^fatal: Path .* exists on disk, but not in .*$/im) + ) { + console.log(`Skipping new package ${pkg.packageJson.name}`); + continue; + } + throw error; + } + } + + return patchVersions; +} + /** * Bumps up the versions of packages to account for * the base versions that are set in .changeset/patched.json. * This may be needed when we have made emergency releases. */ -async function updatePatchVersions() { - const patchedJsonPath = path.resolve('.changeset', 'patched.json'); - const { currentReleaseVersion } = await fs.readJson(patchedJsonPath); - if (Object.keys(currentReleaseVersion).length === 0) { - console.log('No currentReleaseVersion overrides found, skipping.'); - return; - } - - const { packages } = await getPackages(path.resolve('.')); - +async function applyPatchVersions(repo, patchVersions) { const pendingVersionBumps = new Map(); - for (const [name, version] of Object.entries(currentReleaseVersion)) { - const pkg = packages.find(p => p.packageJson.name === name); + for (const [name, version] of patchVersions) { + const pkg = repo.packages.find(p => p.packageJson.name === name); if (!pkg) { throw new Error(`Package ${name} not found`); } @@ -81,7 +139,7 @@ async function updatePatchVersions() { }); } - for (const { dir, packageJson } of packages) { + for (const { dir, packageJson } of [repo.root, ...repo.packages]) { let hasChanges = false; if (pendingVersionBumps.has(packageJson.name)) { @@ -117,20 +175,45 @@ async function updatePatchVersions() { }); } } +} - await fs.writeJSON( - patchedJsonPath, - { currentReleaseVersion: {} }, - { spaces: 2, encoding: 'utf8' }, - ); +/** + * Detects any patched packages version since the most recent release on + * the main branch, and then bumps all packages in the repo accordingly. + */ +async function updatePackageVersions(repo) { + const rootPkgPath = path.resolve(repo.root.dir, 'package.json'); + const { version: currentRelease } = await fs.readJson(rootPkgPath); + console.log(`Current release version: ${currentRelease}`); + + const patchRef = await findTipOfPatchBranch(repo, currentRelease); + if (patchRef) { + console.log(`Tip of the patch branch: ${patchRef}`); + + const patchVersions = await detectPatchVersionsForRef(repo, patchRef); + if (patchVersions.size > 0) { + console.log( + `Found ${patchVersions.size} packages that were patched since the last release`, + ); + for (const [name, version] of patchVersions) { + console.log(` ${name}: ${version}`); + } + + await applyPatchVersions(repo, patchVersions); + } else { + console.log('No packages were patched since the last release'); + } + } else { + console.log('No patch branch found'); + } } /** * Returns the mode and tag that is currently set * in the .changeset/pre.json file */ -async function getPreInfo(rootPath) { - const pre = path.join(rootPath, '.changeset', 'pre.json'); +async function getPreInfo(repo) { + const pre = path.join(repo.root.dir, '.changeset', 'pre.json'); if (!(await fs.pathExists(pre))) { return { mode: undefined, tag: undefined }; } @@ -139,26 +222,30 @@ async function getPreInfo(rootPath) { return { mode, tag }; } +/** + * Returns the name of the current git branch + */ +async function getCurrentBranch(repo) { + const { stdout } = await execFile( + 'git', + ['rev-parse', '--abbrev-ref', 'HEAD'], + { cwd: repo.root.dir, shell: true }, + ); + return stdout.trim(); +} + /** * Bumps the release version in the root package.json. * * This takes into account whether we're in pre-release mode or on a patch branch. */ -async function updateBackstageReleaseVersion() { - const rootPath = path.resolve(__dirname, '..'); - const branchName = await execFile( - 'git', - ['rev-parse', '--abbrev-ref', 'HEAD'], - { shell: true }, - ).then(({ stdout }) => stdout.trim()); - const { mode: preMode, tag: preTag } = await getPreInfo(rootPath); +async function updateBackstageReleaseVersion(repo, type) { + const { mode: preMode, tag: preTag } = await getPreInfo(repo); - const packagePath = path.join(rootPath, 'package.json'); - const package = await fs.readJson(packagePath); - const { version: currentVersion } = package; + const { version: currentVersion } = repo.root.packageJson; let nextVersion; - if (MAIN_BRANCHES.includes(branchName)) { + if (type === 'minor') { if (preMode === 'pre') { if (semver.prerelease(currentVersion)) { nextVersion = semver.inc(currentVersion, 'pre', preTag); @@ -170,7 +257,7 @@ async function updateBackstageReleaseVersion() { } else { nextVersion = semver.inc(currentVersion, 'minor'); } - } else { + } else if (type === 'patch') { if (preMode) { throw new Error(`Unexpected pre mode ${preMode} on branch ${branchName}`); } @@ -178,9 +265,9 @@ async function updateBackstageReleaseVersion() { } await fs.writeJson( - packagePath, + path.join(repo.root.dir, 'package.json'), { - ...package, + ...repo.root.packageJson, version: nextVersion, }, { spaces: 2, encoding: 'utf8' }, @@ -188,8 +275,17 @@ async function updateBackstageReleaseVersion() { } async function main() { - await updatePatchVersions(); - await updateBackstageReleaseVersion(); + const repo = await getPackages(__dirname); + const branchName = await getCurrentBranch(repo); + const isMainBranch = MAIN_BRANCHES.includes(branchName); + + console.log(`Current branch: ${branchName}`); + if (isMainBranch) { + console.log('Main release, updating package versions'); + await updatePackageVersions(repo); + } + + await updateBackstageReleaseVersion(repo, isMainBranch ? 'minor' : 'patch'); } main().catch(error => { diff --git a/storybook/.storybook/apis.js b/storybook/.storybook/apis.js index 4c0813a8e9..37cc8eb605 100644 --- a/storybook/.storybook/apis.js +++ b/storybook/.storybook/apis.js @@ -5,10 +5,8 @@ import { GithubAuth, GitlabAuth, GoogleAuth, - OAuth2, OAuthRequestManager, OktaAuth, - Auth0Auth, ConfigReader, LocalStorageFeatureFlags, } from '@backstage/core-app-api'; @@ -20,10 +18,8 @@ import { gitlabAuthApiRef, googleAuthApiRef, identityApiRef, - oauth2ApiRef, oauthRequestApiRef, oktaAuthApiRef, - auth0AuthApiRef, configApiRef, featureFlagsApiRef, } from '@backstage/core-plugin-api'; @@ -59,16 +55,6 @@ const oktaAuthApi = OktaAuth.create({ basePath: '/auth/', oauthRequestApi, }); -const auth0AuthApi = Auth0Auth.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, -}); -const oauth2Api = OAuth2.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, -}); export const apis = [ [configApiRef, configApi], @@ -81,6 +67,4 @@ export const apis = [ [githubAuthApiRef, githubAuthApi], [gitlabAuthApiRef, gitlabAuthApi], [oktaAuthApiRef, oktaAuthApi], - [auth0AuthApiRef, auth0AuthApi], - [oauth2ApiRef, oauth2Api], ]; diff --git a/yarn.lock b/yarn.lock index a6b27ffa82..66e504f077 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1287,10 +1287,10 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime-corejs3@^7.10.2", "@babel/runtime-corejs3@^7.11.2", "@babel/runtime-corejs3@^7.16.3": - version "7.16.8" - resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.16.8.tgz#ea533d96eda6fdc76b1812248e9fbd0c11d4a1a7" - integrity sha512-3fKhuICS1lMz0plI5ktOE/yEtBRMVxplzRkdn6mJQ197XiY0JnrzYV0+Mxozq3JZ8SBV9Ecurmw1XsGbwOf+Sg== +"@babel/runtime-corejs3@^7.10.2", "@babel/runtime-corejs3@^7.11.2", "@babel/runtime-corejs3@^7.16.8": + version "7.17.2" + resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.17.2.tgz#fdca2cd05fba63388babe85d349b6801b008fd13" + integrity sha512-NcKtr2epxfIrNM4VOmPKO46TvDMCBhgi2CrSHaEarrz+Plk2K5r9QemmOFTGpZaoKnWoGH5MO+CzeRsih/Fcgg== dependencies: core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" @@ -1358,8 +1358,33 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@backstage/catalog-client@^0.7.0": + version "0.7.2" + resolved "https://registry.npmjs.org/@backstage/catalog-client/-/catalog-client-0.7.2.tgz#bcfdb2c210e878fbc5833f18e59feae1e7e48330" + integrity sha512-jeJfi4ekwIffi8ozSzvgdv94DZ2kKNwFVhAP0V+63GF6wIspvGuTIwU2uKYH0v9JXA08lwmAsOGnrrwgS6ragA== + dependencies: + "@backstage/catalog-model" "^0.11.0" + "@backstage/errors" "^0.2.2" + cross-fetch "^3.1.5" + "@backstage/catalog-model@^0.10.0": + version "0.10.1" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.10.1.tgz#dcc3415eb4d4ee3d437355c477e85c7479626b3b" + integrity sha512-c004aQeO9cxtSZZc2iBcE6eoqurQLdj7YUm8mHWs8hEaPTA2UPVHawt+wlt89VywkI89X0wF7BuXV2LKVUfXvw== + dependencies: + "@backstage/config" "^0.1.15" + "@backstage/errors" "^0.2.2" + "@backstage/types" "^0.1.3" + "@types/json-schema" "^7.0.5" + ajv "^7.0.3" + json-schema "^0.4.0" + lodash "^4.17.21" + uuid "^8.0.0" + +"@backstage/catalog-model@^0.11.0": version "0.11.0" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.11.0.tgz#f02f86fe74305b49fce300e9c221659f9092d1b7" + integrity sha512-DnmbzKZejvxBSQv1LjA3AZIxwmchir2A9L9MzWH9D+pVKIu4AEt2HItf6g+xhpgk+4GJppETsEgrLI+Iyr6QSA== dependencies: "@backstage/config" "^0.1.15" "@backstage/errors" "^0.2.2" @@ -1371,16 +1396,108 @@ uuid "^8.0.0" "@backstage/catalog-model@^0.9.7": - version "0.11.0" + version "0.9.10" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.9.10.tgz#bd5662e1ad7bd7c9604f3f45d055c99b5b2bb87f" + integrity sha512-KhCjbZKhS5zZhHiGHmBMq6hDGDshMSZOPGXehtdhr6/oW7Ee5fDcOnhMqreCi1Ebm4RIWJhZcRrxO6X1TTi4TQ== dependencies: - "@backstage/config" "^0.1.15" - "@backstage/errors" "^0.2.2" - "@backstage/types" "^0.1.3" + "@backstage/config" "^0.1.13" + "@backstage/errors" "^0.2.0" + "@backstage/types" "^0.1.1" "@types/json-schema" "^7.0.5" + "@types/yup" "^0.29.13" ajv "^7.0.3" json-schema "^0.4.0" lodash "^4.17.21" uuid "^8.0.0" + yup "^0.32.9" + +"@backstage/core-components@^0.8.0", "@backstage/core-components@^0.8.9": + version "0.8.10" + resolved "https://registry.npmjs.org/@backstage/core-components/-/core-components-0.8.10.tgz#6f79c46cdf507fc3a0d764848a4a8aa73af7ce93" + integrity sha512-gGyCPPSdgvzYHWMKlxe/H4yFmEYDQuAtaoT7Y/3+pilcJSIQi9d3oV104BDLC94bdkyOWDsA1tT+PGZtd0p/8g== + dependencies: + "@backstage/config" "^0.1.15" + "@backstage/core-plugin-api" "^0.7.0" + "@backstage/errors" "^0.2.2" + "@backstage/theme" "^0.2.15" + "@material-table/core" "^3.1.0" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.57" + "@react-hookz/web" "^12.3.0" + "@types/react-sparklines" "^1.7.0" + "@types/react-text-truncate" "^0.14.0" + ansi-regex "^6.0.1" + classnames "^2.2.6" + d3-selection "^3.0.0" + d3-shape "^3.0.0" + d3-zoom "^3.0.0" + dagre "^0.8.5" + history "^5.0.0" + immer "^9.0.1" + lodash "^4.17.21" + pluralize "^8.0.0" + prop-types "^15.7.2" + qs "^6.9.4" + rc-progress "3.2.4" + react-helmet "6.1.0" + react-hook-form "^7.12.2" + react-markdown "^8.0.0" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-sparklines "^1.7.0" + react-syntax-highlighter "^15.4.5" + react-text-truncate "^0.17.0" + react-use "^17.3.2" + react-virtualized-auto-sizer "^1.0.6" + react-window "^1.8.6" + remark-gfm "^3.0.1" + zen-observable "^0.8.15" + zod "^3.11.6" + +"@backstage/core-components@^0.9.0": + version "0.9.0" + resolved "https://registry.npmjs.org/@backstage/core-components/-/core-components-0.9.0.tgz#ff98c755853c25a2622713bd9a2f60f2c2c5e658" + integrity sha512-fT2CGvc+GeyElT48J/TpeISXbLLVp1FZIZGiI/kK1i4m5wA0JlOOXDO7rPyc3QykNjmacThZXJIk/wZxhuIrAA== + dependencies: + "@backstage/config" "^0.1.15" + "@backstage/core-plugin-api" "^0.8.0" + "@backstage/errors" "^0.2.2" + "@backstage/theme" "^0.2.15" + "@material-table/core" "^3.1.0" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.57" + "@react-hookz/web" "^12.3.0" + "@types/react-sparklines" "^1.7.0" + "@types/react-text-truncate" "^0.14.0" + ansi-regex "^6.0.1" + classnames "^2.2.6" + d3-selection "^3.0.0" + d3-shape "^3.0.0" + d3-zoom "^3.0.0" + dagre "^0.8.5" + history "^5.0.0" + immer "^9.0.1" + lodash "^4.17.21" + pluralize "^8.0.0" + prop-types "^15.7.2" + qs "^6.9.4" + rc-progress "3.2.4" + react-helmet "6.1.0" + react-hook-form "^7.12.2" + react-markdown "^8.0.0" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-sparklines "^1.7.0" + react-syntax-highlighter "^15.4.5" + react-text-truncate "^0.18.0" + react-use "^17.3.2" + react-virtualized-auto-sizer "^1.0.6" + react-window "^1.8.6" + remark-gfm "^3.0.1" + zen-observable "^0.8.15" + zod "^3.11.6" "@backstage/core-plugin-api@^0.6.0", "@backstage/core-plugin-api@^0.6.1": version "0.6.1" @@ -1395,6 +1512,47 @@ react-router-dom "6.0.0-beta.0" zen-observable "^0.8.15" +"@backstage/core-plugin-api@^0.7.0": + version "0.7.0" + resolved "https://registry.npmjs.org/@backstage/core-plugin-api/-/core-plugin-api-0.7.0.tgz#e53f3aa5bbf074a70fcaf264b04402637008659c" + integrity sha512-SVzrJjvEjWzJgIqfFUjQjqW0RGCzj6zgLULpSgP53GQat70I0wje6E4n0blNU+/q7hpt6PoIT0lgZtqFOJrfYQ== + dependencies: + "@backstage/config" "^0.1.15" + "@backstage/types" "^0.1.3" + "@backstage/version-bridge" "^0.1.2" + history "^5.0.0" + prop-types "^15.7.2" + react-router-dom "6.0.0-beta.0" + zen-observable "^0.8.15" + +"@backstage/integration-react@^0.1.10": + version "0.1.24" + resolved "https://registry.npmjs.org/@backstage/integration-react/-/integration-react-0.1.24.tgz#fc9cb557a6af37086759724f7be46af3cf36f454" + integrity sha512-3F3bo+gi6LFqs7nqIznGwjzt1ExAJyWvYJ+eU71Qtd56YqyKr+QHlLsdhJV5LfuQd7F4NkQk3UndTYBhLCHKkA== + dependencies: + "@backstage/config" "^0.1.15" + "@backstage/core-components" "^0.9.0" + "@backstage/core-plugin-api" "^0.8.0" + "@backstage/integration" "^0.8.0" + "@backstage/theme" "^0.2.15" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.57" + react-use "^17.2.4" + +"@backstage/integration@^0.7.3": + version "0.7.5" + resolved "https://registry.npmjs.org/@backstage/integration/-/integration-0.7.5.tgz#c68848f35db51705b3287b6fa6a259ae4b17bfbc" + integrity sha512-KUoNQLfPaRqQsdfx04IX4d3EIHbJU3tJxoUjVCCDIAThtep6clhY4uoxXIYTUz+bhERUGGJYhMR+1ryEEtwWhw== + dependencies: + "@backstage/config" "^0.1.15" + "@octokit/auth-app" "^3.4.0" + "@octokit/rest" "^18.5.3" + cross-fetch "^3.1.5" + git-url-parse "^11.6.0" + lodash "^4.17.21" + luxon "^2.0.2" + "@backstage/plugin-catalog-react@^0.6.5": version "0.6.15" resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.6.15.tgz#04de88caa4ac2ce2ad000ee34d28b11eda3910b9" @@ -1808,14 +1966,14 @@ ts-node "^9" tslib "^2" -"@eslint/eslintrc@^1.0.5": - version "1.0.5" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" - integrity sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ== +"@eslint/eslintrc@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.0.tgz#7ce1547a5c46dfe56e1e45c3c9ed18038c721c6a" + integrity sha512-igm9SjJHNEJRiUnecP/1R5T3wKLEJ7pL6e2P+GUSfCd0dGjPYYZve08uzw8L2J8foVHFz+NGu12JxRcU2gGo6w== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.2.0" + espree "^9.3.1" globals "^13.9.0" ignore "^4.0.6" import-fresh "^3.2.1" @@ -1973,12 +2131,12 @@ meros "^1.1.4" "@graphql-codegen/cli@^2.3.1": - version "2.3.1" - resolved "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.3.1.tgz#66083293b60e3182603d70031210d59e6f1a16e5" - integrity sha512-xMSvYqFtnRXOp/sVJSyqiFTm70X8ouLXiq5o/R/D3yQtA6NNudAC+Q4oxg9/LZKnRDL6pehwdC8CNnQk0Tf7Sw== + version "2.6.2" + resolved "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.6.2.tgz#a9aa4656141ee0998cae8c7ad7d0bf9ca8e0c9ae" + integrity sha512-UO75msoVgvLEvfjCezM09cQQqp32+mR8Ma1ACsBpr7nroFvHbgcu2ulx1cMovg4sxDBCsvd9Eq/xOOMpARUxtw== dependencies: - "@graphql-codegen/core" "2.4.0" - "@graphql-codegen/plugin-helpers" "^2.3.2" + "@graphql-codegen/core" "2.5.1" + "@graphql-codegen/plugin-helpers" "^2.4.1" "@graphql-tools/apollo-engine-loader" "^7.0.5" "@graphql-tools/code-file-loader" "^7.0.6" "@graphql-tools/git-loader" "^7.0.5" @@ -2008,7 +2166,7 @@ listr "^0.14.3" listr-update-renderer "^0.5.0" log-symbols "^4.0.0" - minimatch "^3.0.4" + minimatch "^4.0.0" mkdirp "^1.0.4" string-env-interpolation "^1.0.1" ts-log "^2.2.3" @@ -2018,32 +2176,32 @@ yaml "^1.10.0" yargs "^17.0.0" -"@graphql-codegen/core@2.4.0": - version "2.4.0" - resolved "https://registry.npmjs.org/@graphql-codegen/core/-/core-2.4.0.tgz#d94dcc088b5e117c847ce5b10c4fe1eb7325e180" - integrity sha512-5RiYE1+07jayp/3w/bkyaCXtfKNeKmRabpPP4aRi369WeH2cH37l2K8NbhkIU+zhpnhoqMID61TO56x2fKldZQ== +"@graphql-codegen/core@2.5.1": + version "2.5.1" + resolved "https://registry.npmjs.org/@graphql-codegen/core/-/core-2.5.1.tgz#e3d50d3449b8c58b74ea08e97faf656a1b7fc8a1" + integrity sha512-alctBVl2hMnBXDLwkgmnFPrZVIiBDsWJSmxJcM4GKg1PB23+xuov35GE47YAyAhQItE1B1fbYnbb1PtGiDZ4LA== dependencies: - "@graphql-codegen/plugin-helpers" "^2.3.2" + "@graphql-codegen/plugin-helpers" "^2.4.1" "@graphql-tools/schema" "^8.1.2" "@graphql-tools/utils" "^8.1.1" tslib "~2.3.0" "@graphql-codegen/graphql-modules-preset@^2.3.2": - version "2.3.2" - resolved "https://registry.npmjs.org/@graphql-codegen/graphql-modules-preset/-/graphql-modules-preset-2.3.2.tgz#df88431e4cff656799b1ab0324e0795606162389" - integrity sha512-O3PPDQejqf3rF9sHqlrl00M+BSIKJAovFWt2zkDr/D3I/XwntR4QdQDKyY9zTotHfPpgJKyxyMzthpdqPD5XUA== + version "2.3.5" + resolved "https://registry.npmjs.org/@graphql-codegen/graphql-modules-preset/-/graphql-modules-preset-2.3.5.tgz#07ef9ef6e66d4fbb76767097a7cc3cace34f8d08" + integrity sha512-y1PcTFr483Imzd346YdmNSvW3FlN4iYj4sRWCRys+WXpiFcXq+D7+1dFZvs75/9bIHoOEeQtusHHuRPy9qJVkA== dependencies: - "@graphql-codegen/plugin-helpers" "^2.3.2" - "@graphql-codegen/visitor-plugin-common" "2.5.2" - "@graphql-tools/utils" "8.5.5" + "@graphql-codegen/plugin-helpers" "^2.4.0" + "@graphql-codegen/visitor-plugin-common" "2.7.1" + "@graphql-tools/utils" "8.6.1" change-case-all "1.0.14" parse-filepath "^1.0.2" tslib "~2.3.0" -"@graphql-codegen/plugin-helpers@^2.3.2", "@graphql-codegen/plugin-helpers@^2.4.0": - version "2.4.1" - resolved "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-2.4.1.tgz#433845a89b0b4b3a2a0e959e0a2cfe444cf7aeac" - integrity sha512-OPMma7aUnES3Dh+M0BfiNBnJLmYuH60EnbULAhufxFDn/Y2OA0Ht/LQok9beX6VN4ASZEMCOAGItJezGJr5DJw== +"@graphql-codegen/plugin-helpers@^2.3.2", "@graphql-codegen/plugin-helpers@^2.4.0", "@graphql-codegen/plugin-helpers@^2.4.1": + version "2.4.2" + resolved "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-2.4.2.tgz#e4f6b74dddcf8a9974fef5ce48562ae0980f9fed" + integrity sha512-LJNvwAPv/sKtI3RnRDm+nPD+JeOfOuSOS4FFIpQCMUCyMnFcchV/CPTTv7tT12fLUpEg6XjuFfDBvOwndti30Q== dependencies: "@graphql-tools/utils" "^8.5.2" change-case-all "1.0.14" @@ -2286,7 +2444,7 @@ unixify "1.0.0" valid-url "1.0.9" -"@graphql-tools/load@^7.3.0": +"@graphql-tools/load@^7.3.0", "@graphql-tools/load@^7.4.1": version "7.5.1" resolved "https://registry.npmjs.org/@graphql-tools/load/-/load-7.5.1.tgz#8c7f846d2185ddc1d44fdfbf1ed9cb678f69e40b" integrity sha512-j9XcLYZPZdl/TzzqA83qveJmwcCxgGizt5L1+C1/Z68brTEmQHLdQCOR3Ma3ewESJt6DU05kSTu2raKaunkjRg== @@ -2296,16 +2454,6 @@ p-limit "3.1.0" tslib "~2.3.0" -"@graphql-tools/load@^7.4.1": - version "7.4.1" - resolved "https://registry.npmjs.org/@graphql-tools/load/-/load-7.4.1.tgz#aa572fcef11d6028097b6ef39c13fa9d62e5a441" - integrity sha512-UvBodW5hRHpgBUBVz5K5VIhJDOTFIbRRAGD6sQ2l9J5FDKBEs3u/6JjZDzbdL96br94D5cEd2Tk6auaHpTn7mQ== - dependencies: - "@graphql-tools/schema" "8.3.1" - "@graphql-tools/utils" "^8.5.1" - p-limit "3.1.0" - tslib "~2.3.0" - "@graphql-tools/merge@^6.0.0", "@graphql-tools/merge@^6.2.12": version "6.2.14" resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.2.14.tgz#694e2a2785ba47558e5665687feddd2935e9d94e" @@ -2419,7 +2567,7 @@ valid-url "1.0.9" ws "7.4.5" -"@graphql-tools/url-loader@^7.0.11": +"@graphql-tools/url-loader@^7.0.11", "@graphql-tools/url-loader@^7.4.2": version "7.7.0" resolved "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-7.7.0.tgz#504f0030c75b61bca4ac07da49e8cd872c316972" integrity sha512-mBBb+aJqI4E0MVEzyfi76Pi/G6lGxGTVt/tP1YtKJly7UnonNoWOtDusdL3zIVAGhGgLsNrLbGhLDbwSd6TV6A== @@ -2444,42 +2592,17 @@ value-or-promise "^1.0.11" ws "^8.3.0" -"@graphql-tools/url-loader@^7.4.2": - version "7.5.3" - resolved "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-7.5.3.tgz#a594be40e3bc68d22f76746356e7f0b8117b7137" - integrity sha512-VKMRJ4TOeVIdulkCLGSBUr4stRRwOGcVRXDeoUF+86K32Ufo0H2V0lz7QwS/bCl8GXV19FMgHZCDl4BMJyOXEA== - dependencies: - "@graphql-tools/delegate" "^8.4.1" - "@graphql-tools/utils" "^8.5.1" - "@graphql-tools/wrap" "^8.3.1" - "@n1ru4l/graphql-live-query" "0.9.0" - "@types/websocket" "1.0.4" - "@types/ws" "^8.0.0" - cross-undici-fetch "^0.0.26" - dset "^3.1.0" - extract-files "11.0.0" - graphql-sse "^1.0.1" - graphql-ws "^5.4.1" - isomorphic-ws "4.0.1" - meros "1.1.4" - subscriptions-transport-ws "^0.11.0" - sync-fetch "0.3.1" - tslib "~2.3.0" - valid-url "1.0.9" - value-or-promise "1.0.11" - ws "8.3.0" - -"@graphql-tools/utils@8.5.3", "@graphql-tools/utils@^8.5.1", "@graphql-tools/utils@^8.5.3": +"@graphql-tools/utils@8.5.3": version "8.5.3" resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.5.3.tgz#404062e62cae9453501197039687749c4885356e" integrity sha512-HDNGWFVa8QQkoQB0H1lftvaO1X5xUaUDk1zr1qDe0xN1NL0E/CrQdJ5UKLqOvH4hkqVUPxQsyOoAZFkaH6rLHg== dependencies: tslib "~2.3.0" -"@graphql-tools/utils@8.5.5": - version "8.5.5" - resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.5.5.tgz#019ddb99719feb19602afdb537c06e463df674a9" - integrity sha512-y7zRXWIUI73X+9/rf/0KzrNFMlpRKFfzLiwdbIeWwgLs+NV9vfUOoVkX8luXX6LwQxhSypHATMiwZGM2ro/wJA== +"@graphql-tools/utils@8.6.1", "@graphql-tools/utils@^8.1.1", "@graphql-tools/utils@^8.3.0", "@graphql-tools/utils@^8.5.1", "@graphql-tools/utils@^8.5.2", "@graphql-tools/utils@^8.5.3", "@graphql-tools/utils@^8.6.0": + version "8.6.1" + resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.1.tgz#52c7eb108f2ca2fd01bdba8eef85077ead1bf882" + integrity sha512-uxcfHCocp4ENoIiovPxUWZEHOnbXqj3ekWc0rm7fUhW93a1xheARNHcNKhwMTR+UKXVJbTFQdGI1Rl5XdyvDBg== dependencies: tslib "~2.3.0" @@ -2492,13 +2615,6 @@ camel-case "4.1.2" tslib "~2.2.0" -"@graphql-tools/utils@^8.1.1", "@graphql-tools/utils@^8.3.0", "@graphql-tools/utils@^8.5.2", "@graphql-tools/utils@^8.6.0": - version "8.6.0" - resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.0.tgz#f424256a1f3b87d1dcf6f9f675739b2d3627be33" - integrity sha512-rnk+RHaOCeWnfekeQGRh5ycXK1ZAI7Nm0pbeLjA3SiysTdqhWyxNCp5ON4Mvtlid84OY/KB253fQq/2rotznCA== - dependencies: - tslib "~2.3.0" - "@graphql-tools/wrap@^7.0.4": version "7.0.8" resolved "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-7.0.8.tgz#ad41e487135ca3ea1ae0ea04bb3f596177fb4f50" @@ -2557,7 +2673,17 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@hot-loader/react-dom@^17.0.2": +"@hot-loader/react-dom-v16@npm:@hot-loader/react-dom@^16.0.2": + version "16.14.0" + resolved "https://registry.npmjs.org/@hot-loader/react-dom/-/react-dom-16.14.0.tgz#3cfc64e40bb78fa623e59b582b8f09dcdaad648a" + integrity sha512-EN9czvcLsMYmSDo5yRKZOAq3ZGRlDpad1gPtX0NdMMomJXcPE3yFSeFzE94X/NjOaiSVimB7LuqPYpkWVaIi4Q== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.19.1" + +"@hot-loader/react-dom-v17@npm:@hot-loader/react-dom@^17.0.2": version "17.0.2" resolved "https://registry.npmjs.org/@hot-loader/react-dom/-/react-dom-17.0.2.tgz#0b24e484093e8f97eb5c72bebdda44fc20bc8400" integrity sha512-G2RZrFhsQClS+bdDh/Ojpk3SgocLPUGnvnJDTQYnmKSSwXtU+Yh+8QMs+Ia3zaAvBiOSpIIDSUxuN69cvKqrWg== @@ -4089,7 +4215,7 @@ outvariant "^1.2.0" strict-event-emitter "^0.2.0" -"@n1ru4l/graphql-live-query@0.9.0", "@n1ru4l/graphql-live-query@^0.9.0": +"@n1ru4l/graphql-live-query@^0.9.0": version "0.9.0" resolved "https://registry.npmjs.org/@n1ru4l/graphql-live-query/-/graphql-live-query-0.9.0.tgz#defaebdd31f625bee49e6745934f36312532b2bc" integrity sha512-BTpWy1e+FxN82RnLz4x1+JcEewVdfmUhV1C6/XYD5AjS7PQp9QFF7K8bCD6gzPTr2l+prvqOyVueQhFJxB1vfg== @@ -4742,6 +4868,13 @@ dependencies: "@react-hookz/deep-equal" "^1.0.1" +"@react-hookz/web@^13.0.0": + version "13.0.0" + resolved "https://registry.npmjs.org/@react-hookz/web/-/web-13.0.0.tgz#7c4d54fb4c1edf885879914d719e35086964e46e" + integrity sha512-sAMTOiOnAvxpJtHea7Vk+2+KJdTC/o77wPII+glnH+astNUSSiduptLaMPeoIXXwqjfJT+IJ9JzWyZPrLpdyFw== + dependencies: + "@react-hookz/deep-equal" "^1.0.1" + "@rjsf/core@^3.2.1": version "3.2.1" resolved "https://registry.npmjs.org/@rjsf/core/-/core-3.2.1.tgz#8a7b24c9a6f01f0ecb093fdfc777172c12b1b009" @@ -6236,9 +6369,9 @@ redux "^4.0.0" "@types/react-sparklines@^1.7.0": - version "1.7.0" - resolved "https://registry.npmjs.org/@types/react-sparklines/-/react-sparklines-1.7.0.tgz#f956d0f7b0e746ad445ce1cd250fe81f8a384684" - integrity sha512-Vd+cME7+Yy3kFNhnid9EBIKiyCQ/at8nqDczIs0UYfIB8AtaRJPqekigv02biOsIbQCvxyvIAIjiTKOC+hHNbA== + version "1.7.2" + resolved "https://registry.npmjs.org/@types/react-sparklines/-/react-sparklines-1.7.2.tgz#c14e80623abd3669a10f18d13f6fb9fbdc322f70" + integrity sha512-N1GwO7Ri5C5fE8+CxhiDntuSw1qYdGytBuedKrCxWpaojXm4WnfygbdBdc5sXGX7feMxDXBy9MNhxoUTwrMl4A== dependencies: "@types/react" "*" @@ -6613,7 +6746,7 @@ dependencies: "@types/node" "*" -"@types/websocket@1.0.4", "@types/websocket@^1.0.4": +"@types/websocket@^1.0.4": version "1.0.4" resolved "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.4.tgz#1dc497280d8049a5450854dd698ee7e6ea9e60b8" integrity sha512-qn1LkcFEKK8RPp459jkjzsfpbsx36BBt3oC3pITYtkoBw/aVX+EZFa5j3ThCRTNpLFvIMr5dSTD4RaMdilIOpA== @@ -6710,15 +6843,23 @@ eslint-utils "^3.0.0" "@typescript-eslint/parser@^5.9.0": - version "5.9.1" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.9.1.tgz#b114011010a87e17b3265ca715e16c76a9834cef" - integrity sha512-PLYO0AmwD6s6n0ZQB5kqPgfvh73p0+VqopQQLuNfi7Lm0EpfKyDalchpVwkE+81k5HeiRrTV/9w1aNHzjD7C4g== + version "5.13.0" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.13.0.tgz#0394ed8f2f849273c0bf4b811994d177112ced5c" + integrity sha512-GdrU4GvBE29tm2RqWOM0P5QfCtgCyN4hXICj/X9ibKED16136l9ZpoJvCL5pSKtmJzA+NRDzQ312wWMejCVVfg== dependencies: - "@typescript-eslint/scope-manager" "5.9.1" - "@typescript-eslint/types" "5.9.1" - "@typescript-eslint/typescript-estree" "5.9.1" + "@typescript-eslint/scope-manager" "5.13.0" + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/typescript-estree" "5.13.0" debug "^4.3.2" +"@typescript-eslint/scope-manager@5.13.0": + version "5.13.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.13.0.tgz#cf6aff61ca497cb19f0397eea8444a58f46156b6" + integrity sha512-T4N8UvKYDSfVYdmJq7g2IPJYCRzwtp74KyDZytkR4OL3NRupvswvmJQJ4CX5tDSurW2cvCc1Ia1qM7d0jpa7IA== + dependencies: + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/visitor-keys" "5.13.0" + "@typescript-eslint/scope-manager@5.9.0": version "5.9.0" resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.9.0.tgz#02dfef920290c1dcd7b1999455a3eaae7a1a3117" @@ -6727,14 +6868,6 @@ "@typescript-eslint/types" "5.9.0" "@typescript-eslint/visitor-keys" "5.9.0" -"@typescript-eslint/scope-manager@5.9.1": - version "5.9.1" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.9.1.tgz#6c27be89f1a9409f284d95dfa08ee3400166fe69" - integrity sha512-8BwvWkho3B/UOtzRyW07ffJXPaLSUKFBjpq8aqsRvu6HdEuzCY57+ffT7QoV4QXJXWSU1+7g3wE4AlgImmQ9pQ== - dependencies: - "@typescript-eslint/types" "5.9.1" - "@typescript-eslint/visitor-keys" "5.9.1" - "@typescript-eslint/type-utils@5.9.0": version "5.9.0" resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.9.0.tgz#fd5963ead04bc9b7af9c3a8e534d8d39f1ce5f93" @@ -6744,15 +6877,28 @@ debug "^4.3.2" tsutils "^3.21.0" +"@typescript-eslint/types@5.13.0": + version "5.13.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.13.0.tgz#da1de4ae905b1b9ff682cab0bed6b2e3be9c04e5" + integrity sha512-LmE/KO6DUy0nFY/OoQU0XelnmDt+V8lPQhh8MOVa7Y5k2gGRd6U9Kp3wAjhB4OHg57tUO0nOnwYQhRRyEAyOyg== + "@typescript-eslint/types@5.9.0": version "5.9.0" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.9.0.tgz#e5619803e39d24a03b3369506df196355736e1a3" integrity sha512-mWp6/b56Umo1rwyGCk8fPIzb9Migo8YOniBGPAQDNC6C52SeyNGN4gsVwQTAR+RS2L5xyajON4hOLwAGwPtUwg== -"@typescript-eslint/types@5.9.1": - version "5.9.1" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.9.1.tgz#1bef8f238a2fb32ebc6ff6d75020d9f47a1593c6" - integrity sha512-SsWegWudWpkZCwwYcKoDwuAjoZXnM1y2EbEerTHho19Hmm+bQ56QG4L4jrtCu0bI5STaRTvRTZmjprWlTw/5NQ== +"@typescript-eslint/typescript-estree@5.13.0": + version "5.13.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.13.0.tgz#b37c07b748ff030a3e93d87c842714e020b78141" + integrity sha512-Q9cQow0DeLjnp5DuEDjLZ6JIkwGx3oYZe+BfcNuw/POhtpcxMTy18Icl6BJqTSd+3ftsrfuVb7mNHRZf7xiaNA== + dependencies: + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/visitor-keys" "5.13.0" + debug "^4.3.2" + globby "^11.0.4" + is-glob "^4.0.3" + semver "^7.3.5" + tsutils "^3.21.0" "@typescript-eslint/typescript-estree@5.9.0": version "5.9.0" @@ -6767,18 +6913,13 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.9.1": - version "5.9.1" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.9.1.tgz#d5b996f49476495070d2b8dd354861cf33c005d6" - integrity sha512-gL1sP6A/KG0HwrahVXI9fZyeVTxEYV//6PmcOn1tD0rw8VhUWYeZeuWHwwhnewnvEMcHjhnJLOBhA9rK4vmb8A== +"@typescript-eslint/visitor-keys@5.13.0": + version "5.13.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.13.0.tgz#f45ff55bcce16403b221ac9240fbeeae4764f0fd" + integrity sha512-HLKEAS/qA1V7d9EzcpLFykTePmOQqOFim8oCvhY3pZgQ8Hi38hYpHd9e5GN6nQBFQNecNhws5wkS9Y5XIO0s/g== dependencies: - "@typescript-eslint/types" "5.9.1" - "@typescript-eslint/visitor-keys" "5.9.1" - debug "^4.3.2" - globby "^11.0.4" - is-glob "^4.0.3" - semver "^7.3.5" - tsutils "^3.21.0" + "@typescript-eslint/types" "5.13.0" + eslint-visitor-keys "^3.0.0" "@typescript-eslint/visitor-keys@5.9.0": version "5.9.0" @@ -6788,14 +6929,6 @@ "@typescript-eslint/types" "5.9.0" eslint-visitor-keys "^3.0.0" -"@typescript-eslint/visitor-keys@5.9.1": - version "5.9.1" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.9.1.tgz#f52206f38128dd4f675cf28070a41596eee985b7" - integrity sha512-Xh37pNz9e9ryW4TVdwiFzmr4hloty8cFj8GTWMXh3Z8swGwyQWeCcNgF0hm6t09iZd6eiZmIf4zHedQVP6TVtg== - dependencies: - "@typescript-eslint/types" "5.9.1" - eslint-visitor-keys "^3.0.0" - "@vscode/sqlite3@^5.0.7": version "5.0.7" resolved "https://registry.npmjs.org/@vscode/sqlite3/-/sqlite3-5.0.7.tgz#358df36bb0e9e735c54785e3e4b9b2dce1d32895" @@ -7284,7 +7417,7 @@ apollo-server-caching@^3.3.0: dependencies: lru-cache "^6.0.0" -apollo-server-core@^3.6.1, apollo-server-core@^3.6.3: +apollo-server-core@^3.6.3: version "3.6.3" resolved "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-3.6.3.tgz#6b12ffa1af8bc8799930f72360090834915033d1" integrity sha512-TFJmAlI6vPp1MHOSXqYkE6leAyMekWv/D/3ma11uETkcd3EPjERGmxtTXPJElMVEkOK9BEElYKthCrH7bjYLuw== @@ -7322,7 +7455,7 @@ apollo-server-errors@^3.3.1: resolved "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-3.3.1.tgz#ba5c00cdaa33d4cbd09779f8cb6f47475d1cd655" integrity sha512-xnZJ5QWs6FixHICXHxUfm+ZWqqxrNuPlQ+kj5m6RtEgIpekOPssH/SD9gf2B4HuWV0QozorrygwZnux8POvyPA== -apollo-server-express@^3.0.0, apollo-server-express@^3.6.1: +apollo-server-express@^3.0.0, apollo-server-express@^3.6.3: version "3.6.3" resolved "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-3.6.3.tgz#5daf58bf0bdf0107ded7cd52c7e6ce6cd32c8b44" integrity sha512-3CjahZ+n+1T7pHH1qW1B6Ns0BzwOMeupAp2u0+M8ruOmE/e7VKn0OSOQQckZ8Z2AcWxWeno9K89fIv3PoSYgYA== @@ -7356,12 +7489,12 @@ apollo-server-types@^3.5.1: apollo-server-env "^4.2.1" apollo-server@^3.0.0: - version "3.6.1" - resolved "https://registry.npmjs.org/apollo-server/-/apollo-server-3.6.1.tgz#29420b1c0cddbf2e18147a3ca7299485f17137a2" - integrity sha512-Y2MY2/WvaTiofVoIR5ZIYt6c6wX8klZRaXI9x+7JBiFV9HMcOuLLpU3+P4r2EVXuN1LLe82m1PgiAYr+a1OmQg== + version "3.6.3" + resolved "https://registry.npmjs.org/apollo-server/-/apollo-server-3.6.3.tgz#0ba0ddb2835ccf27056d20b6f5b83b0ce9545a79" + integrity sha512-kNvOiDNkIaO+MsfR9v40Vz4ArlDdc9VwVKGJy5dniLW9AoDa/tSF99m8ItfGoMypqlRPMgrNGxkMuToBnvYXNQ== dependencies: - apollo-server-core "^3.6.1" - apollo-server-express "^3.6.1" + apollo-server-core "^3.6.3" + apollo-server-express "^3.6.3" express "^4.17.1" aproba@^1.0.3: @@ -7712,9 +7845,9 @@ aws-sdk-mock@^5.2.1: traverse "^0.6.6" aws-sdk@^2.840.0, aws-sdk@^2.928.0, aws-sdk@^2.948.0: - version "2.1065.0" - resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1065.0.tgz#82b6e4e2a6fbccb1767339e309edd4f0daa958e6" - integrity sha512-OFvpXoL104dTFKpU14ILcLDPAlDbkJNIKXnnG2pK+2x++CvzIRJeNyERtUuEo7QMUOwq5U4nIQJKSPt5fBC/HA== + version "2.1088.0" + resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1088.0.tgz#e8ea761eff2b5c42aca6136c5a01fa9b8066f0a2" + integrity sha512-KtGJTvMPJL6QynasbSMNvz7Onc5ejebY6NzuUvgrw6sNohNJDR/3J/0e016ocQwvEq79MNK4v4EsxNi9eMELtg== dependencies: buffer "4.9.2" events "1.1.1" @@ -8034,6 +8167,14 @@ better-path-resolve@1.0.0: dependencies: is-windows "^1.0.0" +better-sqlite3@^7.5.0: + version "7.5.0" + resolved "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-7.5.0.tgz#2a91cb616453f002096743b0e5b66a7021cd1c63" + integrity sha512-6FdG9DoytYGDhLW7VWW1vxjEz7xHkqK6LnaUQYA8d6GHNgZhu9PFX2xwKEEnSBRoT1J4PjTUPeg217ShxNmuPg== + dependencies: + bindings "^1.5.0" + prebuild-install "^7.0.0" + bfj@^7.0.2: version "7.0.2" resolved "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz#1988ce76f3add9ac2913fd8ba47aad9e651bfbb2" @@ -8094,6 +8235,13 @@ binaryextensions@^4.15.0, binaryextensions@^4.16.0: resolved "https://registry.npmjs.org/binaryextensions/-/binaryextensions-4.18.0.tgz#22aeada2d14de062c60e8ca59a504a5636a76ceb" integrity sha512-PQu3Kyv9dM4FnwB7XGj1+HucW+ShvJzJqjuw1JkKVs1mWdwOKVcRjOi+pV9X52A0tNvrPCsPkbFFQb+wE1EAXw== +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + bintrees@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" @@ -9585,16 +9733,11 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.10: resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== -core-js@^3.4.1: +core-js@^3.4.1, core-js@^3.6.5: version "3.21.1" resolved "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94" integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig== -core-js@^3.6.5: - version "3.20.3" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.20.3.tgz#c710d0a676e684522f3db4ee84e5e18a9d11d69a" - integrity sha512-vVl8j8ph6tRS3B8qir40H7yw7voy17xL0piAjlbBUsH7WIfzoedL/ZOr1OV9FyZQLWXsayOJyV4tnRyXR85/ag== - core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -9726,7 +9869,7 @@ cross-fetch@3.1.4: dependencies: node-fetch "2.6.1" -cross-fetch@3.1.5, cross-fetch@^3.0.4, cross-fetch@^3.0.6, cross-fetch@^3.1.3, cross-fetch@^3.1.4, cross-fetch@^3.1.5: +cross-fetch@3.1.5, cross-fetch@^3.0.4, cross-fetch@^3.0.6, cross-fetch@^3.1.3, cross-fetch@^3.1.5: version "3.1.5" resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== @@ -9772,16 +9915,6 @@ cross-undici-fetch@^0.0.20: node-fetch "^2.6.5" undici "^4.9.3" -cross-undici-fetch@^0.0.26: - version "0.0.26" - resolved "https://registry.npmjs.org/cross-undici-fetch/-/cross-undici-fetch-0.0.26.tgz#29d93d56609f4d2334f9d5333d23ef7a242842a7" - integrity sha512-aMDRrLbWr0TGXfY92stlV+XOGpskeqFmWmrKSWsnc8w6gK5LPE83NBh7O7N6gCb2xjwHcm1Yn2nBXMEVH2RBcA== - dependencies: - abort-controller "^3.0.0" - form-data "^4.0.0" - node-fetch "^2.6.5" - undici "^4.9.3" - cross-undici-fetch@^0.1.4: version "0.1.13" resolved "https://registry.npmjs.org/cross-undici-fetch/-/cross-undici-fetch-0.1.13.tgz#807d17ce5c524c21bc0a6486e97ecccb901c6529" @@ -10329,14 +10462,6 @@ d3-zoom@^3.0.0: d3-selection "2 - 3" d3-transition "2 - 3" -d@1, d@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" - integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== - dependencies: - es5-ext "^0.10.50" - type "^1.0.1" - dagre@^0.8.5: version "0.8.5" resolved "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee" @@ -10536,7 +10661,7 @@ deep-equal@^1.0.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" -deep-extend@0.6.0, deep-extend@^0.6.0, deep-extend@~0.6.0: +deep-extend@0.6.0, deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== @@ -10546,7 +10671,7 @@ deep-is@^0.1.3, deep-is@~0.1.3: resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= -deepmerge@^4.2.2: +deepmerge@^4.2.2, deepmerge@~4.2.2: version "4.2.2" resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== @@ -10701,6 +10826,11 @@ detect-libc@^1.0.3: resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -11304,42 +11434,6 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: - version "0.10.53" - resolved "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" - integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== - dependencies: - es6-iterator "~2.0.3" - es6-symbol "~3.1.3" - next-tick "~1.0.0" - -es6-iterator@^2.0.3, es6-iterator@~2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" - integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= - dependencies: - d "1" - es5-ext "^0.10.35" - es6-symbol "^3.1.1" - -es6-symbol@^3.1.1, es6-symbol@~3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" - integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== - dependencies: - d "^1.0.1" - ext "^1.1.2" - -es6-weak-map@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" - integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== - dependencies: - d "1" - es5-ext "^0.10.46" - es6-iterator "^2.0.3" - es6-symbol "^3.1.1" - esbuild-android-arm64@0.14.22: version "0.14.22" resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.22.tgz#fb051169a63307d958aec85ad596cfc7d7770303" @@ -11532,9 +11626,9 @@ escodegen@^2.0.0: source-map "~0.6.1" eslint-config-prettier@^8.3.0: - version "8.3.0" - resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" - integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== + version "8.5.0" + resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" + integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== eslint-formatter-friendly@^7.0.0: version "7.0.0" @@ -11688,10 +11782,10 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153" - integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg== +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" @@ -11708,10 +11802,10 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz#6fbb166a6798ee5991358bc2daa1ba76cc1254a1" - integrity sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ== +eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== eslint-webpack-plugin@^2.6.0: version "2.6.0" @@ -11726,11 +11820,11 @@ eslint-webpack-plugin@^2.6.0: schema-utils "^3.1.1" eslint@^8.6.0: - version "8.7.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.7.0.tgz#22e036842ee5b7cf87b03fe237731675b4d3633c" - integrity sha512-ifHYzkBGrzS2iDU7KjhCAVMGCvF6M3Xfs8X8b37cgrUlDt6bWRTpRh6T/gtSXv1HJ/BUGgmjvNvOEGu85Iif7w== + version "8.10.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.10.0.tgz#931be395eb60f900c01658b278e05b6dae47199d" + integrity sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw== dependencies: - "@eslint/eslintrc" "^1.0.5" + "@eslint/eslintrc" "^1.2.0" "@humanwhocodes/config-array" "^0.9.2" ajv "^6.10.0" chalk "^4.0.0" @@ -11738,10 +11832,10 @@ eslint@^8.6.0: debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.1.0" + eslint-scope "^7.1.1" eslint-utils "^3.0.0" - eslint-visitor-keys "^3.2.0" - espree "^9.3.0" + eslint-visitor-keys "^3.3.0" + espree "^9.3.1" esquery "^1.4.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -11771,14 +11865,14 @@ esm@^3.2.25: resolved "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== -espree@^9.2.0, espree@^9.3.0: - version "9.3.0" - resolved "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz#c1240d79183b72aaee6ccfa5a90bc9111df085a8" - integrity sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ== +espree@^9.3.1: + version "9.3.1" + resolved "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" + integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== dependencies: acorn "^8.7.0" acorn-jsx "^5.3.1" - eslint-visitor-keys "^3.1.0" + eslint-visitor-keys "^3.3.0" esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" @@ -11834,14 +11928,6 @@ etag@~1.8.1: resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -event-emitter@^0.3.5: - version "0.3.5" - resolved "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" - integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= - dependencies: - d "1" - es5-ext "~0.10.14" - event-source-polyfill@^1.0.25: version "1.0.25" resolved "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.25.tgz#d8bb7f99cb6f8119c2baf086d9f6ee0514b6d9c8" @@ -11899,55 +11985,55 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: safe-buffer "^5.1.1" "example-app@link:packages/app": - version "0.2.66" + version "0.2.68-next.0" dependencies: - "@backstage/app-defaults" "^0.1.9" - "@backstage/catalog-model" "^0.11.0" - "@backstage/cli" "^0.14.1" - "@backstage/core-app-api" "^0.5.4" - "@backstage/core-components" "^0.8.10" - "@backstage/core-plugin-api" "^0.7.0" - "@backstage/integration-react" "^0.1.23" - "@backstage/plugin-airbrake" "^0.3.0" - "@backstage/plugin-apache-airflow" "^0.1.8" - "@backstage/plugin-api-docs" "^0.8.0" - "@backstage/plugin-azure-devops" "^0.1.16" - "@backstage/plugin-badges" "^0.2.24" - "@backstage/plugin-catalog" "^0.9.0" - "@backstage/plugin-catalog-common" "^0.1.4" - "@backstage/plugin-catalog-graph" "^0.2.12" - "@backstage/plugin-catalog-import" "^0.8.3" - "@backstage/plugin-catalog-react" "^0.7.0" - "@backstage/plugin-circleci" "^0.3.0" - "@backstage/plugin-cloudbuild" "^0.3.0" - "@backstage/plugin-code-coverage" "^0.1.27" - "@backstage/plugin-cost-insights" "^0.11.22" - "@backstage/plugin-explore" "^0.3.31" - "@backstage/plugin-gcp-projects" "^0.3.19" - "@backstage/plugin-github-actions" "^0.5.0" - "@backstage/plugin-gocd" "^0.1.6" - "@backstage/plugin-graphiql" "^0.2.32" - "@backstage/plugin-home" "^0.4.16" - "@backstage/plugin-jenkins" "^0.6.0" - "@backstage/plugin-kafka" "^0.3.0" - "@backstage/plugin-kubernetes" "^0.6.0" - "@backstage/plugin-lighthouse" "^0.3.0" - "@backstage/plugin-newrelic" "^0.3.18" - "@backstage/plugin-newrelic-dashboard" "^0.1.8" - "@backstage/plugin-org" "^0.5.0" - "@backstage/plugin-pagerduty" "0.3.27" - "@backstage/plugin-permission-react" "^0.3.2" - "@backstage/plugin-rollbar" "^0.4.0" - "@backstage/plugin-scaffolder" "^0.13.0" - "@backstage/plugin-search" "^0.7.1" - "@backstage/plugin-sentry" "^0.3.38" - "@backstage/plugin-shortcuts" "^0.2.1" - "@backstage/plugin-tech-insights" "^0.1.10" - "@backstage/plugin-tech-radar" "^0.5.7" - "@backstage/plugin-techdocs" "^0.14.0" - "@backstage/plugin-todo" "^0.2.2" - "@backstage/plugin-user-settings" "^0.3.21" - "@backstage/search-common" "^0.2.4" + "@backstage/app-defaults" "^0.2.1-next.0" + "@backstage/catalog-model" "^0.13.0-next.0" + "@backstage/cli" "^0.15.2-next.0" + "@backstage/core-app-api" "^0.6.0" + "@backstage/core-components" "^0.9.1-next.0" + "@backstage/core-plugin-api" "^0.8.0" + "@backstage/integration-react" "^0.1.25-next.0" + "@backstage/plugin-airbrake" "^0.3.2-next.0" + "@backstage/plugin-apache-airflow" "^0.1.10-next.0" + "@backstage/plugin-api-docs" "^0.8.2-next.0" + "@backstage/plugin-azure-devops" "^0.1.18-next.0" + "@backstage/plugin-badges" "^0.2.26-next.0" + "@backstage/plugin-catalog" "^0.10.0-next.0" + "@backstage/plugin-catalog-common" "^0.2.2-next.0" + "@backstage/plugin-catalog-graph" "^0.2.14-next.0" + "@backstage/plugin-catalog-import" "^0.8.5-next.0" + "@backstage/plugin-catalog-react" "^0.9.0-next.0" + "@backstage/plugin-circleci" "^0.3.2-next.0" + "@backstage/plugin-cloudbuild" "^0.3.2-next.0" + "@backstage/plugin-code-coverage" "^0.1.29-next.0" + "@backstage/plugin-cost-insights" "^0.11.24-next.0" + "@backstage/plugin-explore" "^0.3.33-next.0" + "@backstage/plugin-gcp-projects" "^0.3.21-next.0" + "@backstage/plugin-github-actions" "^0.5.2-next.0" + "@backstage/plugin-gocd" "^0.1.8-next.0" + "@backstage/plugin-graphiql" "^0.2.34-next.0" + "@backstage/plugin-home" "^0.4.18-next.0" + "@backstage/plugin-jenkins" "^0.7.1-next.0" + "@backstage/plugin-kafka" "^0.3.2-next.0" + "@backstage/plugin-kubernetes" "^0.6.2-next.0" + "@backstage/plugin-lighthouse" "^0.3.2-next.0" + "@backstage/plugin-newrelic" "^0.3.20-next.0" + "@backstage/plugin-newrelic-dashboard" "^0.1.10-next.0" + "@backstage/plugin-org" "^0.5.2-next.0" + "@backstage/plugin-pagerduty" "0.3.29-next.0" + "@backstage/plugin-permission-react" "^0.3.3" + "@backstage/plugin-rollbar" "^0.4.2-next.0" + "@backstage/plugin-scaffolder" "^0.15.0-next.0" + "@backstage/plugin-search" "^0.7.3-next.0" + "@backstage/plugin-search-common" "^0.3.1-next.0" + "@backstage/plugin-sentry" "^0.3.40-next.0" + "@backstage/plugin-shortcuts" "^0.2.3-next.0" + "@backstage/plugin-tech-insights" "^0.1.12-next.0" + "@backstage/plugin-tech-radar" "^0.5.9-next.0" + "@backstage/plugin-techdocs" "^0.15.1-next.0" + "@backstage/plugin-todo" "^0.2.4-next.0" + "@backstage/plugin-user-settings" "^0.4.1-next.0" "@backstage/theme" "^0.2.15" "@material-ui/core" "^4.12.2" "@material-ui/icons" "^4.9.1" @@ -12065,6 +12151,11 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + expect@^26.6.2: version "26.6.2" resolved "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" @@ -12151,13 +12242,6 @@ express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" -ext@^1.1.2: - version "1.4.0" - resolved "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" - integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A== - dependencies: - type "^2.0.0" - extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -12206,16 +12290,16 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-files@11.0.0, extract-files@^11.0.0: - version "11.0.0" - resolved "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz#b72d428712f787eef1f5193aff8ab5351ca8469a" - integrity sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ== - extract-files@9.0.0, extract-files@^9.0.0: version "9.0.0" resolved "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a" integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ== +extract-files@^11.0.0: + version "11.0.0" + resolved "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz#b72d428712f787eef1f5193aff8ab5351ca8469a" + integrity sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ== + extract-zip@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" @@ -12422,6 +12506,11 @@ file-type@^9.0.0: resolved "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz#a68d5ad07f486414dfb2c8866f73161946714a18" integrity sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw== +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + filelist@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" @@ -12756,25 +12845,15 @@ fs-constants@^1.0.0: resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@10.0.0, fs-extra@^10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" - integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== +fs-extra@10.0.1, fs-extra@^10.0.0, fs-extra@^10.0.1: + version "10.0.1" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" + integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@9.1.0, fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs-extra@^7.0.1, fs-extra@~7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" @@ -12793,6 +12872,16 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^9.0.0, fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-minipass@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" @@ -13084,6 +13173,11 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= + glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -13480,7 +13574,7 @@ handle-thing@^2.0.0: resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@^4.7.3, handlebars@^4.7.6, handlebars@^4.7.7: +handlebars@^4.7.3, handlebars@^4.7.7: version "4.7.7" resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== @@ -14758,7 +14852,7 @@ is-promise@4.0.0, is-promise@^4.0.0: resolved "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== -is-promise@^2.1.0, is-promise@^2.2.2: +is-promise@^2.1.0: version "2.2.2" resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== @@ -16125,9 +16219,9 @@ keyv@^3.0.0: json-buffer "3.0.0" keyv@^4.0.0, keyv@^4.0.3: - version "4.1.0" - resolved "https://registry.npmjs.org/keyv/-/keyv-4.1.0.tgz#8ab5ca4ae6a34e05c629531d9a7f871575af0d5b" - integrity sha512-YsY3wr6HabE11/sscee+3nZ03XjvkrPWGouAmJFBdZoK92wiOlJCzI5/sDEIKdJhdhHO144ei45U9gXfbu14Uw== + version "4.1.1" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.1.1.tgz#02c538bfdbd2a9308cc932d4096f05ae42bfa06a" + integrity sha512-tGv1yP6snQVDSM4X6yxrv2zzq/EvpW+oYiUz6aueW1u9CtS8RzUQYxxmFwgZlO2jSgCxQbchhxaqXXp2hnKGpQ== dependencies: json-buffer "3.0.1" @@ -16235,9 +16329,9 @@ ldap-filter@^0.3.3: assert-plus "^1.0.0" ldapjs@^2.2.0: - version "2.3.1" - resolved "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.1.tgz#04136815fb1f21d692ac87fab5961a04d86e8b04" - integrity sha512-kf0tHHLrpwKaBAQOhYHXgdeh2PkFuCCxWgLb1MRn67ZQVo787D2pij3mmHVZx193GIdM8xcfi8HF6AIYYnj0fQ== + version "2.3.2" + resolved "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.2.tgz#a599d081519f70462941cc33a50e9354c32f35b7" + integrity sha512-FU+GR/qbQ96WUZ2DUb7FzaEybYvv3240wTVPcbsdELB3o4cK92zGVjntsh68siVkLeCmlCcsd/cIQzyGXSS7LA== dependencies: abstract-logging "^2.0.0" asn1 "^0.2.4" @@ -16898,13 +16992,6 @@ lru-cache@^7.3.1: resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.3.1.tgz#7702e80694ec2bf19865567a469f2b081fcf53f5" integrity sha512-nX1x4qUrKqwbIAhv4s9et4FIUVzNOpeY07bsjGUy8gwJrXH/wScImSQqXErmo/b2jZY2r0mohbLA9zVj7u1cNw== -lru-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" - integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM= - dependencies: - es5-ext "~0.10.2" - lunr@^2.3.9: version "2.3.9" resolved "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" @@ -17288,20 +17375,6 @@ memoize-one@^5.1.1: resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== -memoizee@^0.4.15: - version "0.4.15" - resolved "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72" - integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ== - dependencies: - d "^1.0.1" - es5-ext "^0.10.53" - es6-weak-map "^2.0.3" - event-emitter "^0.3.5" - is-promise "^2.2.2" - lru-queue "^0.1.0" - next-tick "^1.1.0" - timers-ext "^0.1.7" - meow@^6.0.0: version "6.1.1" resolved "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" @@ -17797,13 +17870,20 @@ minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimatch@5.0.0, minimatch@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.0.0.tgz#281d8402aaaeed18a9e8406ad99c46a19206c6ef" - integrity sha512-EU+GCVjXD00yOUf1TwAHVP7v3fBD3A8RkkPYsWWKGWesxM/572sL53wJQnHxquHlRhYUV36wHkqrN8cdikKc2g== +minimatch@5.0.1, minimatch@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== dependencies: brace-expansion "^2.0.1" +minimatch@^4.0.0: + version "4.2.1" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + minimist-options@4.1.0, minimist-options@^4.0.2: version "4.1.0" resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -17920,6 +18000,11 @@ mkdirp-classic@^0.5.2: resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz#54c441ce4c96cd7790e10b41a87aa51068ecab2b" integrity sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g== +mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp-infer-owner@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz#55d3b368e7d89065c38f32fd38e638f0ab61d316" @@ -18139,15 +18224,10 @@ nanoclone@^0.2.1: resolved "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== -nanoid@^3.1.23: - version "3.2.0" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" - integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== - -nanoid@^3.2.0: - version "3.3.0" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.0.tgz#5906f776fd886c66c24f3653e0c46fcb1d4ad6b0" - integrity sha512-JzxqqT5u/x+/KOFSd7JP15DOo9nOoHpx6DYatqIHUW2+flybkm+mdcraotSQR5WcnZr+qhGVh8Ted0KdfSMxlg== +nanoid@^3.1.23, nanoid@^3.3.1: + version "3.3.1" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== nanomatch@^1.2.9: version "1.2.13" @@ -18166,6 +18246,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -18186,16 +18271,6 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.2: resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -next-tick@1, next-tick@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" - integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== - -next-tick@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" - integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= - nice-try@^1.0.4: version "1.0.5" resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -18220,6 +18295,13 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +node-abi@^3.3.0: + version "3.8.0" + resolved "https://registry.npmjs.org/node-abi/-/node-abi-3.8.0.tgz#679957dc8e7aa47b0a02589dbfde4f77b29ccb32" + integrity sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw== + dependencies: + semver "^7.3.5" + node-abort-controller@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" @@ -18598,7 +18680,7 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -npmlog@^4.1.2: +npmlog@^4.0.1, npmlog@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -20142,11 +20224,11 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^ integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== postcss@^8.1.0, postcss@^8.4.5: - version "8.4.6" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1" - integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA== + version "8.4.7" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.7.tgz#f99862069ec4541de386bf57f5660a6c7a0875a8" + integrity sha512-L9Ye3r6hkkCeOETQX6iOaWZgjp3LL6Lpqm6EtgbKrgqGGteRMNb9vzBfRL96YOSu8o7x3MfIH9Mo5cPJFGrW6A== dependencies: - nanoid "^3.2.0" + nanoid "^3.3.1" picocolors "^1.0.0" source-map-js "^1.0.2" @@ -20172,6 +20254,25 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" +prebuild-install@^7.0.0: + version "7.0.1" + resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.0.1.tgz#c10075727c318efe72412f333e0ef625beaf3870" + integrity sha512-QBSab31WqkyxpnMWQxubYAHR5S9B2+r81ucocew34Fkl98FhvKIF50jIJnNOBmAZfyNV7vE5T6gd3hTVWgY6tg== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + npmlog "^4.0.1" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + precond@0.2: version "0.2.3" resolved "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" @@ -20650,9 +20751,9 @@ raw-body@2.4.3: unpipe "1.0.0" raw-body@^2.4.1: - version "2.5.0" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.0.tgz#865890d9435243e9fe6141feb4decf929a6e1525" - integrity sha512-XpyZ6O7PVu3ItMQl0LslfsRoKxMOxi3SzDkrOtxMES5AqLFpYjQCryxI4LGygUN2jL+RgFsPkMPPlG7cg/47+A== + version "2.5.1" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== dependencies: bytes "3.1.2" http-errors "2.0.0" @@ -20677,7 +20778,7 @@ rc-util@^5.16.1: react-is "^16.12.0" shallowequal "^1.1.0" -rc@^1.2.8: +rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -20977,6 +21078,13 @@ react-test-renderer@^16.13.1: react-is "^16.8.6" scheduler "^0.19.1" +react-text-truncate@^0.17.0: + version "0.17.0" + resolved "https://registry.npmjs.org/react-text-truncate/-/react-text-truncate-0.17.0.tgz#a820bfd9d084caf85d900a011fe2ab4216fc3821" + integrity sha512-EUL7s47XApOgbR//t/9X+fXg1feS47RcTywNXEQZAlNL0vrCIYGye1C+mpUgGIIXKTkabweid6z7s16AkTo5sA== + dependencies: + prop-types "^15.5.7" + react-text-truncate@^0.18.0: version "0.18.0" resolved "https://registry.npmjs.org/react-text-truncate/-/react-text-truncate-0.18.0.tgz#c65f4be660d24734badb903a4832467eddcf8058" @@ -21732,10 +21840,10 @@ requires-port@^1.0.0: resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= -reselect@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" - integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== +reselect@^4.1.5: + version "4.1.5" + resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.5.tgz#852c361247198da6756d07d9296c2b51eddb79f6" + integrity sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ== resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1: version "1.5.1" @@ -21930,11 +22038,6 @@ rollup-plugin-esbuild@^4.7.2: joycon "^3.0.1" jsonc-parser "^3.0.0" -rollup-plugin-peer-deps-external@^2.2.2: - version "2.2.4" - resolved "https://registry.npmjs.org/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.4.tgz#8a420bbfd6dccc30aeb68c9bf57011f2f109570d" - integrity sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g== - rollup-plugin-postcss@*, rollup-plugin-postcss@^4.0.0: version "4.0.2" resolved "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.2.tgz#15e9462f39475059b368ce0e49c800fa4b1f7050" @@ -22454,6 +22557,15 @@ simple-get@^3.0.2, simple-get@^3.0.3: once "^1.3.1" simple-concat "^1.0.0" +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -22729,7 +22841,7 @@ source-map-resolve@^0.6.0: atob "^2.1.2" decode-uri-component "^0.2.0" -source-map-support@^0.5.10: +source-map-support@^0.5.10, source-map-support@^0.5.16, source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -22737,14 +22849,6 @@ source-map-support@^0.5.10: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@^0.5.16, source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.20: - version "0.5.20" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" - integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" @@ -23480,16 +23584,16 @@ svgo@^2.5.0, svgo@^2.7.0: picocolors "^1.0.0" stable "^0.1.8" -swagger-client@^3.17.0: - version "3.18.0" - resolved "https://registry.npmjs.org/swagger-client/-/swagger-client-3.18.0.tgz#2e59e666b38ded983e26fb512421ef8ff82547f0" - integrity sha512-lNfwTXHim0QiCNuZ4BKgWle7N7+9WlFLtcP02n0xSchFtdzsKJb2kWsOlwplRU3appVFjnHRy+1eVabRc3ZhbA== +swagger-client@^3.18.4: + version "3.18.4" + resolved "https://registry.npmjs.org/swagger-client/-/swagger-client-3.18.4.tgz#71be9df585157a3335a542c407733d2134fa75e9" + integrity sha512-Wj26oEctONq/u0uM+eSj18675YM5e2vFnx7Kr4neLeXEHKUsfceVQ/OdtrBXdrT3VbtdBbZfMTfl1JOBpix2MA== dependencies: "@babel/runtime-corejs3" "^7.11.2" btoa "^1.2.1" cookie "~0.4.1" - cross-fetch "^3.1.4" - deep-extend "~0.6.0" + cross-fetch "^3.1.5" + deepmerge "~4.2.2" fast-json-patch "^3.0.0-1" form-data-encoder "^1.4.3" formdata-node "^4.0.0" @@ -23501,11 +23605,11 @@ swagger-client@^3.17.0: url "~0.11.0" swagger-ui-react@^4.1.3: - version "4.1.3" - resolved "https://registry.npmjs.org/swagger-ui-react/-/swagger-ui-react-4.1.3.tgz#a722ecbe54ef237fa9080447a7c708c4c72d846a" - integrity sha512-o1AoXUTNH40cxWus0QOeWQ8x9tSIEmrLBrOgAOHDnvWJ1qyjT8PjgHjPbUVjMbja18coyuaAAeUdyLKvLGmlDA== + version "4.6.1" + resolved "https://registry.npmjs.org/swagger-ui-react/-/swagger-ui-react-4.6.1.tgz#25d0dc380f73fcba00df214f50adc940910f480c" + integrity sha512-NIVzdzzaHnoG6NxK15A0wpd7TiGPpcK4BBh5mgLq6/hCrytjpeQZrkSM1Yf0p1fD7+rkvKcI+b33w/6RCLt3yw== dependencies: - "@babel/runtime-corejs3" "^7.16.3" + "@babel/runtime-corejs3" "^7.16.8" "@braintree/sanitize-url" "^5.0.2" base64-js "^1.5.1" classnames "^2.3.1" @@ -23517,7 +23621,6 @@ swagger-ui-react@^4.1.3: js-file-download "^0.4.12" js-yaml "=4.1.0" lodash "^4.17.21" - memoizee "^0.4.15" prop-types "^15.7.2" randombytes "^2.1.0" react-copy-to-clipboard "5.0.4" @@ -23530,11 +23633,11 @@ swagger-ui-react@^4.1.3: redux "^4.1.2" redux-immutable "^4.0.0" remarkable "^2.0.1" - reselect "^4.0.0" + reselect "^4.1.5" serialize-error "^8.1.0" sha.js "^2.4.11" - swagger-client "^3.17.0" - url-parse "^1.5.3" + swagger-client "^3.18.4" + url-parse "^1.5.8" xml "=1.0.1" xml-but-prettier "^1.0.1" zenscroll "^4.0.2" @@ -23587,7 +23690,7 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar-fs@2.1.1, tar-fs@^2.1.1: +tar-fs@2.1.1, tar-fs@^2.0.0, tar-fs@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== @@ -23656,19 +23759,19 @@ tdigest@^0.1.1: bintrees "1.0.1" "techdocs-cli-embedded-app@link:packages/techdocs-cli-embedded-app": - version "0.2.65" + version "0.2.67-next.0" dependencies: - "@backstage/app-defaults" "^0.1.9" - "@backstage/catalog-model" "^0.11.0" - "@backstage/cli" "^0.14.1" + "@backstage/app-defaults" "^0.2.1-next.0" + "@backstage/catalog-model" "^0.13.0-next.0" + "@backstage/cli" "^0.15.2-next.0" "@backstage/config" "^0.1.15" - "@backstage/core-app-api" "^0.5.4" - "@backstage/core-components" "^0.8.10" - "@backstage/core-plugin-api" "^0.7.0" - "@backstage/integration-react" "^0.1.23" - "@backstage/plugin-catalog" "^0.9.0" - "@backstage/plugin-techdocs" "^0.14.0" - "@backstage/test-utils" "^0.2.6" + "@backstage/core-app-api" "^0.6.0" + "@backstage/core-components" "^0.9.1-next.0" + "@backstage/core-plugin-api" "^0.8.0" + "@backstage/integration-react" "^0.1.25-next.0" + "@backstage/plugin-catalog" "^0.10.0-next.0" + "@backstage/plugin-techdocs" "^0.15.1-next.0" + "@backstage/test-utils" "^0.3.0" "@backstage/theme" "^0.2.15" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -23869,14 +23972,6 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" -timers-ext@^0.1.7: - version "0.1.7" - resolved "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6" - integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ== - dependencies: - es5-ext "~0.10.46" - next-tick "1" - timm@^1.6.1: version "1.7.1" resolved "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz#96bab60c7d45b5a10a8a4d0f0117c6b7e5aff76f" @@ -24140,9 +24235,9 @@ ts-log@^2.2.3: integrity sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w== ts-node@^10.0.0, ts-node@^10.2.1, ts-node@^10.4.0: - version "10.5.0" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.5.0.tgz#618bef5854c1fbbedf5e31465cbb224a1d524ef9" - integrity sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw== + version "10.7.0" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" + integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== dependencies: "@cspotcode/source-map-support" "0.7.0" "@tsconfig/node10" "^1.0.7" @@ -24313,16 +24408,6 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -type@^1.0.1: - version "1.2.0" - resolved "https://registry.npmjs.org/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" - integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== - -type@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3" - integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow== - typed-rest-client@^1.8.4: version "1.8.4" resolved "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.4.tgz#ba3fb788e5b9322547406392533f12d660a5ced6" @@ -24728,7 +24813,7 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.5.3: +url-parse@^1.5.8: version "1.5.10" resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== @@ -25503,16 +25588,16 @@ ws@7.4.5: resolved "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1" integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g== -ws@8.3.0, "ws@^5.2.0 || ^6.0.0 || ^7.0.0", ws@^7.2.3, ws@^7.3.1, ws@^7.4.6, ws@^8.3.0: - version "7.5.6" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" - integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== - ws@8.5.0, ws@^8.1.0: version "8.5.0" resolved "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== +"ws@^5.2.0 || ^6.0.0 || ^7.0.0", ws@^7.2.3, ws@^7.3.1, ws@^7.4.6, ws@^8.3.0: + version "7.5.6" + resolved "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" + integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== + ws@~7.4.2: version "7.4.6" resolved "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"