@backstage/plugin-search-backend-module-stack-overflow-collator 0.0.0-nightly-20231020021216
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +95 -0
- package/config.d.ts +51 -0
- package/dist/index.cjs.js +167 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/package.json +52 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @backstage/plugin-search-backend-module-stack-overflow-collator
|
|
2
|
+
|
|
3
|
+
## 0.0.0-nightly-20231020021216
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 46f0f1700eb8: Extract a package for the Stack Overflow new backend system plugin.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies
|
|
12
|
+
- @backstage/plugin-search-backend-node@0.0.0-nightly-20231020021216
|
|
13
|
+
- @backstage/backend-common@0.0.0-nightly-20231020021216
|
|
14
|
+
- @backstage/config@1.1.1
|
|
15
|
+
- @backstage/backend-tasks@0.0.0-nightly-20231020021216
|
|
16
|
+
- @backstage/backend-plugin-api@0.0.0-nightly-20231020021216
|
|
17
|
+
- @backstage/plugin-search-common@1.2.7
|
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Stack Overflow Search Backend Module
|
|
2
|
+
|
|
3
|
+
A plugin that provides stack overflow specific functionality that can be used in different ways (e.g. for search) to compose your Backstage App.
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
Before we begin, make sure:
|
|
8
|
+
|
|
9
|
+
- 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/).
|
|
10
|
+
|
|
11
|
+
To use any of the functionality this plugin provides, you need to start by configuring your App with the following config:
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
stackoverflow:
|
|
15
|
+
baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Stack Overflow for Teams
|
|
19
|
+
|
|
20
|
+
If you have a private Stack Overflow instance and/or a private Stack Overflow Team you will need to supply an API key or Personal Access Token. You can read more about how to set this up by going to [Stack Overflow's Help Page](https://stackoverflow.help/en/articles/4385859-stack-overflow-for-teams-api).
|
|
21
|
+
|
|
22
|
+
The existing API key approach remains the default, to support the new v2.3 API and PAT authentication model you need to pass the team name and the new PAT into the existing apiAccessToken parameter to the new URL. See [15770](https://github.com/backstage/backstage/issues/15770) for more details.
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
stackoverflow:
|
|
26
|
+
baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance
|
|
27
|
+
apiKey: $STACK_OVERFLOW_API_KEY
|
|
28
|
+
apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
stackoverflow:
|
|
33
|
+
baseUrl: https://api.stackoverflowteams.com/2.3 # alternative: your internal stack overflow instance
|
|
34
|
+
teamName: $STACK_OVERFLOW_TEAM_NAME
|
|
35
|
+
apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Areas of Responsibility
|
|
39
|
+
|
|
40
|
+
This stack overflow backend plugin is primarily responsible for the following:
|
|
41
|
+
|
|
42
|
+
- Provides a `StackOverflowQuestionsCollatorFactory`, which can be used in the search backend to index stack overflow questions to your Backstage Search.
|
|
43
|
+
|
|
44
|
+
### Index Stack Overflow Questions to search
|
|
45
|
+
|
|
46
|
+
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).
|
|
47
|
+
|
|
48
|
+
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 optionally modify the `requestParams`, otherwise it will defaults to `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as done in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions).
|
|
49
|
+
|
|
50
|
+
> Note: if your `baseUrl` is set to the external stack overflow api `https://api.stackexchange.com/2.2`, you can find optional and required parameters under the official API documentation under [`Usage of /questions GET`](https://api.stackexchange.com/docs/questions)
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
indexBuilder.addCollator({
|
|
54
|
+
schedule,
|
|
55
|
+
factory: StackOverflowQuestionsCollatorFactory.fromConfig(env.config, {
|
|
56
|
+
logger: env.logger,
|
|
57
|
+
requestParams: {
|
|
58
|
+
tagged: ['backstage'],
|
|
59
|
+
site: 'stackoverflow',
|
|
60
|
+
pagesize: 100,
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## New Backend System
|
|
67
|
+
|
|
68
|
+
> DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below.
|
|
69
|
+
|
|
70
|
+
This package exports a module that extends the search backend to also indexing the questions exposed by the [`Stack Overflow` API](https://api.stackexchange.com/docs/questions).
|
|
71
|
+
|
|
72
|
+
### Installation
|
|
73
|
+
|
|
74
|
+
Add the module package as a dependency:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# From your Backstage root directory
|
|
78
|
+
yarn add --cwd packages/backend @backstage/plugin-search-backend-module-stack-overflow-collator
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Add the collator to your backend instance, along with the search plugin itself:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
// packages/backend/src/index.ts
|
|
85
|
+
import { createBackend } from '@backstage/backend-defaults';
|
|
86
|
+
|
|
87
|
+
const backend = createBackend();
|
|
88
|
+
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
|
89
|
+
backend.add(
|
|
90
|
+
import('@backstage/plugin-search-backend-module-stack-overflow-collator'),
|
|
91
|
+
);
|
|
92
|
+
backend.start();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `stackoverflow` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/config.d.ts) for more details.
|
package/config.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 The Backstage Authors
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface Config {
|
|
18
|
+
/**
|
|
19
|
+
* Configuration options for the stack overflow plugin
|
|
20
|
+
*/
|
|
21
|
+
stackoverflow?: {
|
|
22
|
+
/**
|
|
23
|
+
* The base url of the Stack Overflow API used for the plugin
|
|
24
|
+
*/
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The API key to authenticate to Stack Overflow API
|
|
29
|
+
* @visibility secret
|
|
30
|
+
*/
|
|
31
|
+
apiKey?: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The name of the team for a Stack Overflow for Teams account
|
|
35
|
+
*/
|
|
36
|
+
teamName?: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The API Access Token to authenticate to Stack Overflow API
|
|
40
|
+
* @visibility secret
|
|
41
|
+
*/
|
|
42
|
+
apiAccessToken?: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Type representing the request parameters.
|
|
46
|
+
*/
|
|
47
|
+
requestParams?: {
|
|
48
|
+
[key: string]: string | string[] | number;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var stream = require('stream');
|
|
6
|
+
var fetch = require('node-fetch');
|
|
7
|
+
var qs = require('qs');
|
|
8
|
+
var backendCommon = require('@backstage/backend-common');
|
|
9
|
+
var backendTasks = require('@backstage/backend-tasks');
|
|
10
|
+
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
11
|
+
var alpha = require('@backstage/plugin-search-backend-node/alpha');
|
|
12
|
+
|
|
13
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
14
|
+
|
|
15
|
+
var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
|
|
16
|
+
var qs__default = /*#__PURE__*/_interopDefaultLegacy(qs);
|
|
17
|
+
|
|
18
|
+
var __defProp = Object.defineProperty;
|
|
19
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
20
|
+
var __publicField = (obj, key, value) => {
|
|
21
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
24
|
+
class StackOverflowQuestionsCollatorFactory {
|
|
25
|
+
constructor(options) {
|
|
26
|
+
__publicField(this, "requestParams");
|
|
27
|
+
__publicField(this, "baseUrl");
|
|
28
|
+
__publicField(this, "apiKey");
|
|
29
|
+
__publicField(this, "apiAccessToken");
|
|
30
|
+
__publicField(this, "teamName");
|
|
31
|
+
__publicField(this, "maxPage");
|
|
32
|
+
__publicField(this, "logger");
|
|
33
|
+
__publicField(this, "type", "stack-overflow");
|
|
34
|
+
var _a, _b;
|
|
35
|
+
this.baseUrl = options.baseUrl;
|
|
36
|
+
this.apiKey = options.apiKey;
|
|
37
|
+
this.apiAccessToken = options.apiAccessToken;
|
|
38
|
+
this.teamName = options.teamName;
|
|
39
|
+
this.maxPage = options.maxPage;
|
|
40
|
+
this.requestParams = (_b = options.requestParams) != null ? _b : {
|
|
41
|
+
order: "desc",
|
|
42
|
+
sort: "activity",
|
|
43
|
+
site: "stackoverflow",
|
|
44
|
+
...(_a = options.requestParams) != null ? _a : {}
|
|
45
|
+
};
|
|
46
|
+
this.logger = options.logger.child({ documentType: this.type });
|
|
47
|
+
}
|
|
48
|
+
static fromConfig(config, options) {
|
|
49
|
+
var _a;
|
|
50
|
+
const apiKey = config.getOptionalString("stackoverflow.apiKey");
|
|
51
|
+
const apiAccessToken = config.getOptionalString(
|
|
52
|
+
"stackoverflow.apiAccessToken"
|
|
53
|
+
);
|
|
54
|
+
const teamName = config.getOptionalString("stackoverflow.teamName");
|
|
55
|
+
const baseUrl = config.getOptionalString("stackoverflow.baseUrl") || "https://api.stackexchange.com/2.3";
|
|
56
|
+
const maxPage = options.maxPage || 100;
|
|
57
|
+
const requestParams = (_a = config.getOptionalConfig("stackoverflow.requestParams")) == null ? void 0 : _a.get();
|
|
58
|
+
return new StackOverflowQuestionsCollatorFactory({
|
|
59
|
+
baseUrl,
|
|
60
|
+
maxPage,
|
|
61
|
+
apiKey,
|
|
62
|
+
apiAccessToken,
|
|
63
|
+
teamName,
|
|
64
|
+
requestParams,
|
|
65
|
+
...options
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async getCollator() {
|
|
69
|
+
return stream.Readable.from(this.execute());
|
|
70
|
+
}
|
|
71
|
+
async *execute() {
|
|
72
|
+
var _a;
|
|
73
|
+
if (!this.baseUrl) {
|
|
74
|
+
this.logger.debug(
|
|
75
|
+
`No stackoverflow.baseUrl configured in your app-config.yaml`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (this.apiKey && this.teamName) {
|
|
79
|
+
this.logger.debug(
|
|
80
|
+
"Both stackoverflow.apiKey and stackoverflow.teamName configured in your app-config.yaml, apiKey must be removed before teamName will be used"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
if (Object.keys(this.requestParams).indexOf("key") >= 0) {
|
|
85
|
+
this.logger.warn(
|
|
86
|
+
"The API Key should be passed as a separate param to bypass encoding"
|
|
87
|
+
);
|
|
88
|
+
delete this.requestParams.key;
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
this.logger.error(`Caught ${e}`);
|
|
92
|
+
}
|
|
93
|
+
const params = qs__default["default"].stringify(this.requestParams, {
|
|
94
|
+
arrayFormat: "comma",
|
|
95
|
+
addQueryPrefix: true
|
|
96
|
+
});
|
|
97
|
+
const apiKeyParam = this.apiKey ? `${params ? "&" : "?"}key=${this.apiKey}` : "";
|
|
98
|
+
const teamParam = this.teamName ? `${params ? "&" : "?"}team=${this.teamName}` : "";
|
|
99
|
+
const requestUrl = this.apiKey ? `${this.baseUrl}/questions${params}${apiKeyParam}` : `${this.baseUrl}/questions${params}${teamParam}`;
|
|
100
|
+
let hasMorePages = true;
|
|
101
|
+
let page = 1;
|
|
102
|
+
while (hasMorePages) {
|
|
103
|
+
if (page === this.maxPage) {
|
|
104
|
+
this.logger.warn(
|
|
105
|
+
`Over ${this.maxPage} requests to the Stack Overflow API have been made, which may not have been intended. Either specify requestParams that limit the questions returned, or configure a higher maxPage if necessary.`
|
|
106
|
+
);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
const res = await fetch__default["default"](
|
|
110
|
+
`${requestUrl}&page=${page}`,
|
|
111
|
+
this.apiAccessToken ? {
|
|
112
|
+
headers: {
|
|
113
|
+
"X-API-Access-Token": this.apiAccessToken
|
|
114
|
+
}
|
|
115
|
+
} : void 0
|
|
116
|
+
);
|
|
117
|
+
const data = await res.json();
|
|
118
|
+
for (const question of (_a = data.items) != null ? _a : []) {
|
|
119
|
+
yield {
|
|
120
|
+
title: question.title,
|
|
121
|
+
location: question.link,
|
|
122
|
+
text: question.owner.display_name,
|
|
123
|
+
tags: question.tags,
|
|
124
|
+
answers: question.answer_count
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
hasMorePages = data.has_more;
|
|
128
|
+
page = page + 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const searchStackOverflowCollatorModule = backendPluginApi.createBackendModule({
|
|
134
|
+
moduleId: "stackOverflowCollator",
|
|
135
|
+
pluginId: "search",
|
|
136
|
+
register(env) {
|
|
137
|
+
env.registerInit({
|
|
138
|
+
deps: {
|
|
139
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
140
|
+
logger: backendPluginApi.coreServices.logger,
|
|
141
|
+
discovery: backendPluginApi.coreServices.discovery,
|
|
142
|
+
scheduler: backendPluginApi.coreServices.scheduler,
|
|
143
|
+
indexRegistry: alpha.searchIndexRegistryExtensionPoint
|
|
144
|
+
},
|
|
145
|
+
async init({ config, logger, scheduler, indexRegistry }) {
|
|
146
|
+
const defaultSchedule = {
|
|
147
|
+
frequency: { minutes: 10 },
|
|
148
|
+
timeout: { minutes: 15 },
|
|
149
|
+
initialDelay: { seconds: 3 }
|
|
150
|
+
};
|
|
151
|
+
const schedule = config.has("stackoverflow.schedule") ? backendTasks.readTaskScheduleDefinitionFromConfig(
|
|
152
|
+
config.getConfig("stackoverflow.schedule")
|
|
153
|
+
) : defaultSchedule;
|
|
154
|
+
indexRegistry.addCollator({
|
|
155
|
+
schedule: scheduler.createScheduledTaskRunner(schedule),
|
|
156
|
+
factory: StackOverflowQuestionsCollatorFactory.fromConfig(config, {
|
|
157
|
+
logger: backendCommon.loggerToWinstonLogger(logger)
|
|
158
|
+
})
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
exports.StackOverflowQuestionsCollatorFactory = StackOverflowQuestionsCollatorFactory;
|
|
166
|
+
exports["default"] = searchStackOverflowCollatorModule;
|
|
167
|
+
//# sourceMappingURL=index.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../src/collators/StackOverflowQuestionsCollatorFactory.ts","../src/module/SearchStackOverflowCollatorModule.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n IndexableDocument,\n DocumentCollatorFactory,\n} from '@backstage/plugin-search-common';\nimport { Config } from '@backstage/config';\nimport { Readable } from 'stream';\nimport fetch from 'node-fetch';\nimport qs from 'qs';\nimport { Logger } from 'winston';\n\n/**\n * Extended IndexableDocument with stack overflow specific properties\n *\n * @public\n */\nexport interface StackOverflowDocument extends IndexableDocument {\n answers: number;\n tags: string[];\n}\n\n/**\n * Type representing the request parameters accepted by the {@link StackOverflowQuestionsCollatorFactory}\n *\n * @public\n */\nexport type StackOverflowQuestionsRequestParams = {\n [key: string]: string | string[] | number;\n};\n\n/**\n * Options for {@link StackOverflowQuestionsCollatorFactory}\n *\n * @public\n */\nexport type StackOverflowQuestionsCollatorFactoryOptions = {\n baseUrl?: string;\n maxPage?: number;\n apiKey?: string;\n apiAccessToken?: string;\n teamName?: string;\n requestParams?: StackOverflowQuestionsRequestParams;\n logger: Logger;\n};\n\n/**\n * Search collator responsible for collecting stack overflow questions to index.\n *\n * @public\n */\nexport class StackOverflowQuestionsCollatorFactory\n implements DocumentCollatorFactory\n{\n protected requestParams: StackOverflowQuestionsRequestParams;\n private readonly baseUrl: string | undefined;\n private readonly apiKey: string | undefined;\n private readonly apiAccessToken: string | undefined;\n private readonly teamName: string | undefined;\n private readonly maxPage: number | undefined;\n private readonly logger: Logger;\n public readonly type: string = 'stack-overflow';\n\n private constructor(options: StackOverflowQuestionsCollatorFactoryOptions) {\n this.baseUrl = options.baseUrl;\n this.apiKey = options.apiKey;\n this.apiAccessToken = options.apiAccessToken;\n this.teamName = options.teamName;\n this.maxPage = options.maxPage;\n // Sets the same default request parameters as the official API documentation\n // See https://api.stackexchange.com/docs/questions\n this.requestParams = options.requestParams ?? {\n order: 'desc',\n sort: 'activity',\n site: 'stackoverflow',\n ...(options.requestParams ?? {}),\n };\n this.logger = options.logger.child({ documentType: this.type });\n }\n\n static fromConfig(\n config: Config,\n options: StackOverflowQuestionsCollatorFactoryOptions,\n ) {\n const apiKey = config.getOptionalString('stackoverflow.apiKey');\n const apiAccessToken = config.getOptionalString(\n 'stackoverflow.apiAccessToken',\n );\n const teamName = config.getOptionalString('stackoverflow.teamName');\n const baseUrl =\n config.getOptionalString('stackoverflow.baseUrl') ||\n 'https://api.stackexchange.com/2.3';\n const maxPage = options.maxPage || 100;\n const requestParams = config\n .getOptionalConfig('stackoverflow.requestParams')\n ?.get<StackOverflowQuestionsRequestParams>();\n return new StackOverflowQuestionsCollatorFactory({\n baseUrl,\n maxPage,\n apiKey,\n apiAccessToken,\n teamName,\n requestParams,\n ...options,\n });\n }\n\n async getCollator() {\n return Readable.from(this.execute());\n }\n\n async *execute(): AsyncGenerator<StackOverflowDocument> {\n if (!this.baseUrl) {\n this.logger.debug(\n `No stackoverflow.baseUrl configured in your app-config.yaml`,\n );\n }\n\n if (this.apiKey && this.teamName) {\n this.logger.debug(\n 'Both stackoverflow.apiKey and stackoverflow.teamName configured in your app-config.yaml, apiKey must be removed before teamName will be used',\n );\n }\n\n try {\n if (Object.keys(this.requestParams).indexOf('key') >= 0) {\n this.logger.warn(\n 'The API Key should be passed as a separate param to bypass encoding',\n );\n delete this.requestParams.key;\n }\n } catch (e) {\n this.logger.error(`Caught ${e}`);\n }\n\n const params = qs.stringify(this.requestParams, {\n arrayFormat: 'comma',\n addQueryPrefix: true,\n });\n\n const apiKeyParam = this.apiKey\n ? `${params ? '&' : '?'}key=${this.apiKey}`\n : '';\n\n const teamParam = this.teamName\n ? `${params ? '&' : '?'}team=${this.teamName}`\n : '';\n\n // PAT change requires team name as a parameter\n const requestUrl = this.apiKey\n ? `${this.baseUrl}/questions${params}${apiKeyParam}`\n : `${this.baseUrl}/questions${params}${teamParam}`;\n\n let hasMorePages = true;\n let page = 1;\n while (hasMorePages) {\n if (page === this.maxPage) {\n this.logger.warn(\n `Over ${this.maxPage} requests to the Stack Overflow API have been made, which may not have been intended. Either specify requestParams that limit the questions returned, or configure a higher maxPage if necessary.`,\n );\n break;\n }\n const res = await fetch(\n `${requestUrl}&page=${page}`,\n this.apiAccessToken\n ? {\n headers: {\n 'X-API-Access-Token': this.apiAccessToken,\n },\n }\n : undefined,\n );\n\n const data = await res.json();\n for (const question of data.items ?? []) {\n yield {\n title: question.title,\n location: question.link,\n text: question.owner.display_name,\n tags: question.tags,\n answers: question.answer_count,\n };\n }\n hasMorePages = data.has_more;\n page = page + 1;\n }\n }\n}\n","/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { loggerToWinstonLogger } from '@backstage/backend-common';\nimport { readTaskScheduleDefinitionFromConfig } from '@backstage/backend-tasks';\nimport {\n coreServices,\n createBackendModule,\n} from '@backstage/backend-plugin-api';\nimport { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha';\nimport { StackOverflowQuestionsCollatorFactory } from '../collators';\n\n/**\n * @public\n * Search backend module for the Stack Overflow index.\n */\nexport const searchStackOverflowCollatorModule = createBackendModule({\n moduleId: 'stackOverflowCollator',\n pluginId: 'search',\n register(env) {\n env.registerInit({\n deps: {\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n discovery: coreServices.discovery,\n scheduler: coreServices.scheduler,\n indexRegistry: searchIndexRegistryExtensionPoint,\n },\n async init({ config, logger, scheduler, indexRegistry }) {\n const defaultSchedule = {\n frequency: { minutes: 10 },\n timeout: { minutes: 15 },\n initialDelay: { seconds: 3 },\n };\n\n const schedule = config.has('stackoverflow.schedule')\n ? readTaskScheduleDefinitionFromConfig(\n config.getConfig('stackoverflow.schedule'),\n )\n : defaultSchedule;\n\n indexRegistry.addCollator({\n schedule: scheduler.createScheduledTaskRunner(schedule),\n factory: StackOverflowQuestionsCollatorFactory.fromConfig(config, {\n logger: loggerToWinstonLogger(logger),\n }),\n });\n },\n });\n },\n});\n"],"names":["Readable","qs","fetch","createBackendModule","coreServices","searchIndexRegistryExtensionPoint","readTaskScheduleDefinitionFromConfig","loggerToWinstonLogger"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { IndexableDocument, DocumentCollatorFactory } from '@backstage/plugin-search-common';
|
|
3
|
+
import { Config } from '@backstage/config';
|
|
4
|
+
import { Readable } from 'stream';
|
|
5
|
+
import { Logger } from 'winston';
|
|
6
|
+
import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extended IndexableDocument with stack overflow specific properties
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
interface StackOverflowDocument extends IndexableDocument {
|
|
14
|
+
answers: number;
|
|
15
|
+
tags: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Type representing the request parameters accepted by the {@link StackOverflowQuestionsCollatorFactory}
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
type StackOverflowQuestionsRequestParams = {
|
|
23
|
+
[key: string]: string | string[] | number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Options for {@link StackOverflowQuestionsCollatorFactory}
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
type StackOverflowQuestionsCollatorFactoryOptions = {
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
maxPage?: number;
|
|
33
|
+
apiKey?: string;
|
|
34
|
+
apiAccessToken?: string;
|
|
35
|
+
teamName?: string;
|
|
36
|
+
requestParams?: StackOverflowQuestionsRequestParams;
|
|
37
|
+
logger: Logger;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Search collator responsible for collecting stack overflow questions to index.
|
|
41
|
+
*
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
declare class StackOverflowQuestionsCollatorFactory implements DocumentCollatorFactory {
|
|
45
|
+
protected requestParams: StackOverflowQuestionsRequestParams;
|
|
46
|
+
private readonly baseUrl;
|
|
47
|
+
private readonly apiKey;
|
|
48
|
+
private readonly apiAccessToken;
|
|
49
|
+
private readonly teamName;
|
|
50
|
+
private readonly maxPage;
|
|
51
|
+
private readonly logger;
|
|
52
|
+
readonly type: string;
|
|
53
|
+
private constructor();
|
|
54
|
+
static fromConfig(config: Config, options: StackOverflowQuestionsCollatorFactoryOptions): StackOverflowQuestionsCollatorFactory;
|
|
55
|
+
getCollator(): Promise<Readable>;
|
|
56
|
+
execute(): AsyncGenerator<StackOverflowDocument>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @public
|
|
61
|
+
* Search backend module for the Stack Overflow index.
|
|
62
|
+
*/
|
|
63
|
+
declare const searchStackOverflowCollatorModule: () => _backstage_backend_plugin_api.BackendFeature;
|
|
64
|
+
|
|
65
|
+
export { StackOverflowDocument, StackOverflowQuestionsCollatorFactory, StackOverflowQuestionsCollatorFactoryOptions, StackOverflowQuestionsRequestParams, searchStackOverflowCollatorModule as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@backstage/plugin-search-backend-module-stack-overflow-collator",
|
|
3
|
+
"description": "A module for the search backend that exports stack overflow modules",
|
|
4
|
+
"version": "0.0.0-nightly-20231020021216",
|
|
5
|
+
"main": "dist/index.cjs.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public",
|
|
10
|
+
"main": "dist/index.cjs.js",
|
|
11
|
+
"types": "dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"backstage": {
|
|
14
|
+
"role": "backend-plugin-module"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://backstage.io",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/backstage/backstage",
|
|
20
|
+
"directory": "plugins/search-backend-module-stack-overflow"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"start": "backstage-cli package start",
|
|
24
|
+
"build": "backstage-cli package build",
|
|
25
|
+
"lint": "backstage-cli package lint",
|
|
26
|
+
"test": "backstage-cli package test",
|
|
27
|
+
"prepack": "backstage-cli package prepack",
|
|
28
|
+
"postpack": "backstage-cli package postpack",
|
|
29
|
+
"clean": "backstage-cli package clean"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@backstage/backend-common": "^0.0.0-nightly-20231020021216",
|
|
33
|
+
"@backstage/backend-plugin-api": "^0.0.0-nightly-20231020021216",
|
|
34
|
+
"@backstage/backend-tasks": "^0.0.0-nightly-20231020021216",
|
|
35
|
+
"@backstage/config": "^1.1.1",
|
|
36
|
+
"@backstage/plugin-search-backend-node": "^0.0.0-nightly-20231020021216",
|
|
37
|
+
"@backstage/plugin-search-common": "^1.2.7",
|
|
38
|
+
"node-fetch": "^2.6.7",
|
|
39
|
+
"qs": "^6.9.4",
|
|
40
|
+
"winston": "^3.2.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@backstage/backend-test-utils": "^0.0.0-nightly-20231020021216",
|
|
44
|
+
"@backstage/cli": "^0.0.0-nightly-20231020021216",
|
|
45
|
+
"msw": "^1.2.1"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"config.d.ts"
|
|
50
|
+
],
|
|
51
|
+
"configSchema": "config.d.ts"
|
|
52
|
+
}
|