diff --git a/plugins/stack-overflow-backend/.eslintrc.js b/plugins/stack-overflow-backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/stack-overflow-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/stack-overflow-backend/README.md b/plugins/stack-overflow-backend/README.md new file mode 100644 index 0000000000..bc4ecb7041 --- /dev/null +++ b/plugins/stack-overflow-backend/README.md @@ -0,0 +1,43 @@ +# Stack Overflow + +A plugin that provides stack overflow specific functionality that can be used in different ways (e.g. for search) to compose your Backstage App. + +## Getting started + +Before we begin, make sure: + +- You have created your own standalone Backstage app using @backstage/create-app and not using a fork of the backstage repository. If you haven't setup Backstage already, start [here](https://backstage.io/docs/getting-started/). + +To use any of the functionality this plugin provides, you need to start by configuring your App with the following config: + +```yaml +stackoverflow: + baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance +``` + +## Areas of Responsibility + +This stack overflow backend plugin is primarily responsible for the following: + +- Provides a `StackOverflowQuestionsCollatorFactory`, which can be used in the search backend to index stack overflow questions to your Backstage Search. + +### Index Stack Overflow Questions to search + +Before you are able to start index stack overflow questions to search, you need to go through the [search getting started guide](https://backstage.io/docs/features/search/getting-started). + +When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, add the following code snippet to add the `StackOverflowQuestionsCollatorFactory`. Note that you can modify the `requestParams`. + +> Note: if your baseUrl is set to the external stack overflow api `https://api.stackexchange.com/2.2`, you can find optional and required params under the official API documentation under [`Usage of /questions GET`](https://api.stackexchange.com/docs/questions) + +```ts +indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + factory: StackOverflowQuestionsCollatorFactory.fromConfig(config, { + requestParams: { + tagged: ['backstage'], + site: 'stackoverflow', + pagesize: 100, + }, + }), +}); +``` diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json new file mode 100644 index 0000000000..3f8727fca6 --- /dev/null +++ b/plugins/stack-overflow-backend/package.json @@ -0,0 +1,45 @@ +{ + "name": "@backstage/plugin-stack-overflow-backend", + "version": "0.0.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" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/stack-overflow-backend" + }, + "keywords": [ + "backstage", + "stack-overflow" + ], + "scripts": { + "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/search-common": "^0.2.4", + "@backstage/config": "^0.1.15", + "qs": "^6.10.3", + "cross-fetch": "^3.1.5", + "winston": "^3.6.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/stack-overflow/src/search/StackOverflowQuestionsCollator/index.ts b/plugins/stack-overflow-backend/src/index.ts similarity index 87% rename from plugins/stack-overflow/src/search/StackOverflowQuestionsCollator/index.ts rename to plugins/stack-overflow-backend/src/index.ts index d5fecd0dce..9b7e7881fd 100644 --- a/plugins/stack-overflow/src/search/StackOverflowQuestionsCollator/index.ts +++ b/plugins/stack-overflow-backend/src/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { StackOverflowQuestionsCollator } from './StackOverflowQuestionsCollator'; +export { StackOverflowQuestionsCollatorFactory } from './search'; diff --git a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts new file mode 100644 index 0000000000..85b0deee24 --- /dev/null +++ b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts @@ -0,0 +1,90 @@ +/* + * 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, + DocumentCollatorFactory, +} from '@backstage/search-common'; +import { Config } from '@backstage/config'; +import { Readable } from 'stream'; +import fetch from 'cross-fetch'; +import qs from 'qs'; +import { Logger } from 'winston'; + +interface StackOverflowDocument extends IndexableDocument { + answers: number; + tags: string[]; +} + +type StackOverflowQuestionsRequestParams = { + [key: string]: string | string[] | number; +}; + +type StackOverflowQuestionsCollatorFactoryOptions = { + baseUrl?: string; + requestParams: StackOverflowQuestionsRequestParams; + logger: Logger; +}; + +export class StackOverflowQuestionsCollatorFactory + implements DocumentCollatorFactory +{ + protected requestParams: StackOverflowQuestionsRequestParams; + private readonly baseUrl: string | undefined; + private readonly logger: Logger; + public readonly type: string = 'stack-overflow'; + + private constructor(options: StackOverflowQuestionsCollatorFactoryOptions) { + this.baseUrl = options.baseUrl; + this.requestParams = options.requestParams; + this.logger = options.logger; + } + + static fromConfig( + config: Config, + options: StackOverflowQuestionsCollatorFactoryOptions, + ) { + const baseUrl = config.getString('stackoverflow.baseUrl'); + return new StackOverflowQuestionsCollatorFactory({ ...options, baseUrl }); + } + + async getCollator() { + return Readable.from(this.execute()); + } + + async *execute(): AsyncGenerator { + if (!this.baseUrl) { + this.logger.debug( + `No stackoverflow.baseUrl configured in your app-config.yaml`, + ); + } + const params = this.requestParams + ? `?${qs.stringify(this.requestParams, { arrayFormat: 'comma' })}` + : ''; + const res = await fetch(`${this.baseUrl}/questions${params}`); + const data = await res.json(); + + for (const question of data.items) { + yield { + title: question.title, + location: question.link, + text: question.owner.display_name, + tags: question.tags, + answers: question.answer_count, + }; + } + } +} diff --git a/plugins/stack-overflow-backend/src/search/index.ts b/plugins/stack-overflow-backend/src/search/index.ts new file mode 100644 index 0000000000..875a945b44 --- /dev/null +++ b/plugins/stack-overflow-backend/src/search/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 { StackOverflowQuestionsCollatorFactory } from './StackOverflowQuestionsCollatorFactory'; diff --git a/plugins/stack-overflow/README.md b/plugins/stack-overflow/README.md index 0a4fbea101..9925605a83 100644 --- a/plugins/stack-overflow/README.md +++ b/plugins/stack-overflow/README.md @@ -17,30 +17,9 @@ stackoverflow: ## Areas of Responsibility -This search plugin is primarily responsible for the following: +This stack overflow frontend plugin is primarily responsible for the following: - Exposing various stack-overflow related components like `` which can be used for composing the search page, and `` which can be used for composing the homepage. -- Provides a `StackOverflowQuestionsCollator`, which can be used in the search backend to index stack overflow questions to your Backstage Search. - -### Index Stack Overflow Questions to search - -Before you are able to start index stack overflow questions to search, you need to go through the [search getting started guide](https://backstage.io/docs/features/search/getting-started). - -When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, add the following code snippet to add the `StackOverflowQuestionsCollator`. Note that you can modify the `requestParams`. - -```ts -indexBuilder.addCollator({ - defaultRefreshIntervalSeconds: 60, - collator: new StackOverflowQuestionsCollator({ - config, - requestParams: { - tagged: ['backstage'], - site: 'stackoverflow', - pagesize: 100, - }, - }), -}); -``` #### Use specific search result list item for Stack Overflow Question diff --git a/plugins/stack-overflow/src/index.ts b/plugins/stack-overflow/src/index.ts index 9e71988100..fd88491fc6 100644 --- a/plugins/stack-overflow/src/index.ts +++ b/plugins/stack-overflow/src/index.ts @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { stackOverflowPlugin, StackOverflowSearchResultListItem, HomePageStackOverflowQuestions, } from './plugin'; -export { StackOverflowQuestionsCollator } from './search/StackOverflowQuestionsCollator'; diff --git a/plugins/stack-overflow/src/search/StackOverflowQuestionsCollator/StackOverflowQuestionsCollator.tsx b/plugins/stack-overflow/src/search/StackOverflowQuestionsCollator/StackOverflowQuestionsCollator.tsx deleted file mode 100644 index a44b2f4742..0000000000 --- a/plugins/stack-overflow/src/search/StackOverflowQuestionsCollator/StackOverflowQuestionsCollator.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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, DocumentCollator } from '@backstage/search-common'; -import { Config } from '@backstage/config'; -import fetch from 'cross-fetch'; -import qs from 'qs'; -import { - StackOverflowQuestion, - StackOverflowQuestionsRequestParams, -} from '../../types'; - -interface StackOverflowDocument extends IndexableDocument { - answers: number; - tags: string[]; -} - -export class StackOverflowQuestionsCollator implements DocumentCollator { - protected baseUrl: string; - protected requestParams: StackOverflowQuestionsRequestParams; - public readonly type: string = 'stack-overflow'; - - constructor({ - config, - requestParams, - }: { - config: Config; - requestParams: StackOverflowQuestionsRequestParams; - }) { - this.baseUrl = config.getString('stackoverflow.baseUrl'); - this.requestParams = requestParams; - } - - async execute() { - const params = this.requestParams - ? `?${qs.stringify(this.requestParams)}` - : ''; - const res = await fetch(`${this.baseUrl}/questions${params}`); - - const data = await res.json(); - return data.items.map( - ({ - title, - link, - owner: { display_name }, - tags, - answer_count, - }: StackOverflowQuestion): StackOverflowDocument => { - return { - title: title, - location: link, - text: display_name, - tags: tags, - answers: answer_count, - }; - }, - ); - } -}