@red-hat-developer-hub/backstage-plugin-bulk-import-backend 7.2.0 → 7.3.0

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +41 -4
  3. package/dist/database/repositoryDao.cjs.js +6 -0
  4. package/dist/database/repositoryDao.cjs.js.map +1 -1
  5. package/dist/generated/openapidocument.cjs.js +89 -0
  6. package/dist/generated/openapidocument.cjs.js.map +1 -1
  7. package/dist/github/GithubAppManager.cjs.js +3 -0
  8. package/dist/github/GithubAppManager.cjs.js.map +1 -1
  9. package/dist/github/githubApiService.cjs.js +140 -144
  10. package/dist/github/githubApiService.cjs.js.map +1 -1
  11. package/dist/github/types.cjs.js.map +1 -1
  12. package/dist/github/utils/ghUtils.cjs.js +3 -3
  13. package/dist/github/utils/ghUtils.cjs.js.map +1 -1
  14. package/dist/github/utils/repoUtils.cjs.js +53 -99
  15. package/dist/github/utils/repoUtils.cjs.js.map +1 -1
  16. package/dist/gitlab/GitlabAppManager.cjs.js +2 -0
  17. package/dist/gitlab/GitlabAppManager.cjs.js.map +1 -1
  18. package/dist/gitlab/gitlabApiService.cjs.js +65 -8
  19. package/dist/gitlab/gitlabApiService.cjs.js.map +1 -1
  20. package/dist/gitlab/utils/glUtils.cjs.js +1 -1
  21. package/dist/gitlab/utils/glUtils.cjs.js.map +1 -1
  22. package/dist/gitlab/utils/repoUtils.cjs.js +32 -57
  23. package/dist/gitlab/utils/repoUtils.cjs.js.map +1 -1
  24. package/dist/service/handlers/import/bulkImports.cjs.js +14 -6
  25. package/dist/service/handlers/import/bulkImports.cjs.js.map +1 -1
  26. package/dist/service/handlers/import/importStatus.cjs.js.map +1 -1
  27. package/dist/service/handlers/organization/organizations.cjs.js +2 -2
  28. package/dist/service/handlers/organization/organizations.cjs.js.map +1 -1
  29. package/dist/service/handlers/repository/repositories.cjs.js +41 -6
  30. package/dist/service/handlers/repository/repositories.cjs.js.map +1 -1
  31. package/dist/service/handlers/scm/scm.cjs.js +16 -0
  32. package/dist/service/handlers/scm/scm.cjs.js.map +1 -0
  33. package/dist/service/router.cjs.js +81 -9
  34. package/dist/service/router.cjs.js.map +1 -1
  35. package/package.json +16 -16
package/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # @red-hat-developer-hub/backstage-plugin-bulk-import-backend
2
2
 
3
+ ## 7.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4eafd59: **BREAKING** Changes the behavior of the bulk-import backend plugin to return only repositories that are yet to be imported by filtering out the already imported ones. Therefore, the frontend will not display already imported repositories with status displayed as "Imported" anymore. The frontend fetches all repositories at once on the first page load and then all the pagination and search is done client-side.
8
+ - a1ae6d2: ## On Behalf of User Access
9
+
10
+ This release introduces the ability for the Bulk Import plugin to fetch repository and organization listings **on behalf of the signed-in user**, using their OAuth credentials rather than relying solely on server-side integration credentials (GitHub App, PAT, or GitLab token).
11
+
12
+ ### What Changed
13
+
14
+ **Backend (`bulk-import-backend`)**
15
+ - Added a new `GET /api/bulk-import/scm-hosts` endpoint that returns the configured GitHub and GitLab integration host URLs as a `SCMHostList` object, enabling the frontend to discover which hosts to request OAuth tokens for.
16
+ - The `GET /repositories` and `GET /organizations/{organizationName}/repositories` endpoints now **require** the `x-scm-tokens` request header — a JSON map of SCM host base URL to user OAuth token. Requests that omit this header, or supply an empty or oversized header, are rejected with HTTP 401. This ensures repository listings are always scoped to the signed-in user's access and never fall back to server-wide integration credentials.
17
+ - The `x-scm-tokens` header is stripped from the request immediately upon receipt, before the permission check and before any audit event is created, so OAuth token values are never persisted in audit logs.
18
+ - When user tokens are provided for GitHub, the Octokit response cache is intentionally disabled to prevent cross-user ETag cache leakage. Server-side credential paths are not affected.
19
+ - Introduced a shared `GitApiService` interface and common SCM types (`SCMOrganization`, `SCMRepository`, `SCMFetchError`, etc.) to unify the GitHub and GitLab service implementations under a consistent contract.
20
+
21
+ **Frontend (`bulk-import`)**
22
+ - The plugin now has a **soft dependency** on `@backstage/integration-react`'s `ScmAuthApi`. If the API is registered in the application, the plugin automatically requests OAuth tokens for each configured SCM host and passes them to the backend to enable user-scoped repository listings.
23
+ - Added `getSCMHosts()` to the `BulkImportAPI` interface with a corresponding `GET /api/bulk-import/scm-hosts` client call, used to discover host URLs before requesting user tokens.
24
+ - User OAuth tokens are transmitted to the backend via the `X-SCM-Tokens` request header as a JSON-encoded map.
25
+ - If the SCM OAuth integration is not configured or token collection fails for all hosts, the repository list query is **blocked** on the frontend and the hook surfaces a descriptive error. This prevents the frontend from firing a request that will always be rejected with 401.
26
+
27
+ ### Required Configuration
28
+
29
+ The GitHub and/or GitLab OAuth provider must be configured in the Backstage application for repository listing to work. Deployments that previously relied on server-side credentials alone for the repository list view must add an SCM OAuth provider to continue using this feature.
30
+
31
+ If `ScmAuthApi` is not registered or tokens cannot be obtained for any configured SCM host, users will see an error prompting them to configure the SCM OAuth integration.
32
+
33
+ - 328508c: Backstage version bump to v1.49.3
34
+
35
+ ### Patch Changes
36
+
37
+ - b3a0333: Fixed GitLab OAuth flow for on behalf of the signed-in user
38
+ - 518943d: Updated dependency `@openapitools/openapi-generator-cli` to `2.31.1`.
39
+ Updated dependency `@playwright/test` to `1.59.1`.
40
+ Updated dependency `@red-hat-developer-hub/backstage-plugin-theme` to `^0.14.0`.
41
+ - 8e0bb08: Updated dependency `@openapitools/openapi-generator-cli` to `2.30.2`.
42
+ Updated dependency `openapicmd` to `2.9.0`.
43
+ Updated dependency `@playwright/test` to `1.58.2`.
44
+ - 3f9d1fe: Updated dependency `openapicmd` to `2.9.2`.
45
+ - Updated dependencies [328508c]
46
+ - @red-hat-developer-hub/backstage-plugin-bulk-import-common@7.3.0
47
+
48
+ ## 7.2.1
49
+
50
+ ### Patch Changes
51
+
52
+ - @red-hat-developer-hub/backstage-plugin-bulk-import-common@7.2.1
53
+
3
54
  ## 7.2.0
4
55
 
5
56
  ### Patch Changes
package/README.md CHANGED
@@ -301,13 +301,15 @@ The Bulk Import Backend plugin emits audit events for various operations. Events
301
301
 
302
302
  - **`ping`**: tracks `GET` requests to the `/ping` endpoint, which allows to make sure the bulk import backend is up and running.
303
303
 
304
- - **`org-read`**: tracks `GET` requests to the `/organizations` endpoint, which returns the list of organizations accessible from all configured GitHub Integrations.
304
+ - **`scm-hosts-read`**: tracks `GET` requests to the `/scm-hosts-read` endpoint, which returns the list of configured GitHub and GitLab integration host URLs.
305
+
306
+ - **`org-read`**: tracks `GET` requests to the `/organizations` endpoint, which returns the list of organizations accessible from all configured SCM Integrations (GitHub and GitLab).
305
307
 
306
308
  Filter on `queryType`.
307
309
  - **`all`**: tracks fetching all organizations. (GET `/organizations`)
308
310
  - **`by-query`**: tracks fetching organization filtered by the query parameter 'search'. (GET `/organizations`)
309
311
 
310
- - **`repo-read`**: tracks `GET` requests to the endpoint, which returns the list of repositories accessible from all configured GitHub Integrations.
312
+ - **`repo-read`**: tracks `GET` requests to the endpoint, which returns the list of repositories accessible from all configured SCM Integrations (GitHub and GitLab).
311
313
 
312
314
  Filter on `queryType`.
313
315
  - **`all`**: tracks fetching a list of all repositories accessible by Backstage Github Integrations. (GET `/repositories`)
@@ -343,8 +345,43 @@ Example:
343
345
 
344
346
  The bulk import backend plugin provides a REST API to bulk import catalog entities into the catalog. The API is available at the `/api/bulk-import` endpoint.
345
347
 
346
- As a prerequisite, you need to add at least one GitHub Integration (using either a GitHub token or a GitHub App or both) in your app-config YAML file (or a local `app-config.local.yaml` file).
347
- See https://backstage.io/docs/integrations/github/locations/#configuration and https://backstage.io/docs/integrations/github/github-apps/#including-in-integrations-config for more details.
348
+ As a prerequisite, you need to add at least one SCM integration in your app-config YAML file (or a local `app-config.local.yaml` file):
349
+
350
+ - **GitHub**: Configure a GitHub integration using a GitHub token or a GitHub App (or both). See the [GitHub Locations](https://backstage.io/docs/integrations/github/locations/#configuration) and [GitHub Apps](https://backstage.io/docs/integrations/github/github-apps/#including-in-integrations-config) documentation for details.
351
+ - **GitLab** _(optional)_: Configure a GitLab integration if you want to import from GitLab repositories. See the [GitLab Locations](https://backstage.io/docs/integrations/gitlab/locations/) documentation for details.
352
+
353
+ ### On Behalf of User Access
354
+
355
+ The plugin supports fetching repository and organization listings **on behalf of the signed-in user**, using their OAuth credentials rather than the server-wide integration credentials (GitHub App, PAT, or GitLab token).
356
+
357
+ #### How It Works
358
+
359
+ 1. The frontend calls `GET /api/bulk-import/scm-hosts` to retrieve the list of configured SCM integration host URLs, grouped by provider (`github` and `gitlab`).
360
+ 2. For each host, the frontend requests an OAuth token from the Backstage `ScmAuthApi` (provided by `@backstage/integration-react`).
361
+ 3. The collected tokens are sent to the backend via the **required** `x-scm-tokens` request header — a JSON-encoded string whose value, when parsed, maps each integration base URL to the user's OAuth token (e.g. `{"https://github.com":"ghp_xxx"}`).
362
+ 4. The backend uses these user tokens to call the GitHub or GitLab APIs on behalf of the user, so the repository listings reflect what the signed-in user can personally access.
363
+
364
+ #### Required OAuth Configuration
365
+
366
+ The `x-scm-tokens` header is **required** for `GET /repositories` and `GET /organizations/{organizationName}/repositories`. Requests that omit the header, supply an empty token map, or send a header that exceeds the allowed size are rejected with **HTTP 401**.
367
+
368
+ A GitHub and/or GitLab OAuth provider must therefore be configured in the Backstage application for these endpoints to work. Refer to the [Backstage GitHub auth docs](https://backstage.io/docs/auth/github/provider) and [GitLab auth docs](https://backstage.io/docs/auth/gitlab/provider) for setup instructions.
369
+
370
+ > **Migration note:** Deployments that previously relied solely on server-side credentials (GitHub App, PAT, or GitLab token) for the repository list view must now also configure an SCM OAuth provider. The server-side credentials are still used for all other operations (import creation, status checks, etc.) and are unaffected by this change.
371
+
372
+ #### Security Note
373
+
374
+ When user tokens are provided for GitHub, the Octokit response cache is intentionally disabled to prevent cross-user ETag cache leakage. Server-side credential paths are not affected.
375
+
376
+ The `x-scm-tokens` header is stripped from the request immediately upon receipt — before the permission check and before any audit event is created — so OAuth token values are never persisted in audit logs.
377
+
378
+ #### New API Endpoint
379
+
380
+ | Method | Path | Description |
381
+ | ------ | ---------------------------- | ------------------------------------------------------------------------------------------- |
382
+ | `GET` | `/api/bulk-import/scm-hosts` | Returns configured GitHub and GitLab integration host base URLs as an `SCMHostList` object. |
383
+
384
+ The existing `GET /repositories` and `GET /organizations/{organizationName}/repositories` endpoints now **require** the `x-scm-tokens` header. See the [API documentation](api-docs/README.md) for the full request/response specification.
348
385
 
349
386
  ## REST API
350
387
 
@@ -30,6 +30,9 @@ class RepositoryDao {
30
30
  this.logger = logger;
31
31
  this.tableName = tableName;
32
32
  }
33
+ knex;
34
+ logger;
35
+ tableName;
33
36
  async findRepositories(page = 1, size = 10, search) {
34
37
  this.logger.debug(
35
38
  `Fetching repositories page=${page}, size=${size}, search=${search}`
@@ -68,6 +71,7 @@ class ScaffolderTaskDao {
68
71
  constructor(knex) {
69
72
  this.knex = knex;
70
73
  }
74
+ knex;
71
75
  async findAllTasks() {
72
76
  return await this.knex("scaffolder_tasks").select(
73
77
  "taskId",
@@ -95,6 +99,7 @@ class TaskLocationsDao {
95
99
  constructor(knex) {
96
100
  this.knex = knex;
97
101
  }
102
+ knex;
98
103
  async addTaskLocation(taskId, location, type = "component") {
99
104
  await this.knex("task_locations").insert({ taskId, location, type });
100
105
  }
@@ -114,6 +119,7 @@ class OrchestratorWorkflowDao {
114
119
  constructor(knex) {
115
120
  this.knex = knex;
116
121
  }
122
+ knex;
117
123
  async insertWorkflow(instanceId, repositoryId) {
118
124
  const [newWorkflow] = await this.knex("orchestrator_workflows").insert({
119
125
  instance_id: instanceId,
@@ -1 +1 @@
1
- {"version":3,"file":"repositoryDao.cjs.js","sources":["../../src/database/repositoryDao.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\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 type { LoggerService } from '@backstage/backend-plugin-api';\n\nimport { Knex } from 'knex';\n\nimport { Components } from '../generated/openapi';\n\nexport interface Repository {\n id: number;\n url: string;\n approvalTool: Components.Schemas.ApprovalTool;\n}\n\nexport interface ScaffolderTask {\n taskId: string;\n scaffolderOptions: any;\n repositoryId: number;\n executedAt?: Date;\n}\n\nexport interface PaginatedResult<T> {\n data: T[];\n page: number;\n size: number;\n total: number;\n totalPages: number;\n}\n\nexport async function paginateQuery<T>(\n queryBuilder: Knex.QueryBuilder<any, T[]>,\n page: number = 1,\n size: number = 10,\n search?: { column: string; term?: string },\n): Promise<PaginatedResult<T>> {\n const offset = (page - 1) * size;\n\n let baseQuery = queryBuilder.clone();\n\n if (search?.term) {\n if (queryBuilder.client.dialect === 'postgres') {\n baseQuery = baseQuery.whereILike(search.column, `%${search.term}%`);\n } else {\n baseQuery = baseQuery.whereLike(search.column, `%${search.term}%`);\n }\n }\n\n const countQuery = baseQuery\n .clone()\n .clearSelect()\n .clearOrder()\n .count<{ count: string }[]>('* as count');\n\n const [rows, [{ count }]] = await Promise.all([\n baseQuery.clone().limit(size).offset(offset),\n countQuery,\n ]);\n\n const total = Number(count);\n\n return {\n data: rows as T[],\n page,\n size,\n total,\n totalPages: Math.ceil(total / size),\n };\n}\n\nexport type RepositoryName = 'repositories' | 'orchestrator_repositories';\n\n// @internal\nexport class RepositoryDao<R extends RepositoryName> {\n constructor(\n private readonly knex: Knex<any, any[]>,\n private readonly logger: LoggerService,\n private readonly tableName: R,\n ) {}\n\n async findRepositories(\n page: number = 1,\n size: number = 10,\n search?: string,\n ): Promise<PaginatedResult<Repository>> {\n this.logger.debug(\n `Fetching repositories page=${page}, size=${size}, search=${search}`,\n );\n const query = this.knex(this.tableName).select('id', 'url', 'approvalTool');\n const searchParam = { column: 'url', term: search };\n return paginateQuery<Repository>(query, page, size, searchParam);\n }\n\n async insertRepository(\n repoUrl: string,\n taskOrWorkflowId: string,\n approvalTool: string,\n ): Promise<number> {\n this.logger.debug(\n `Saving repository ${repoUrl} for task/workflow ${taskOrWorkflowId} to database..`,\n );\n const repository = await this.knex(this.tableName)\n .where({ url: repoUrl })\n .first();\n\n let repositoryId;\n if (repository) {\n repositoryId = repository.id;\n } else {\n const [newRepository] = await this.knex(this.tableName)\n .insert({ url: repoUrl, approvalTool: approvalTool })\n .returning('id');\n repositoryId = newRepository.id;\n }\n return repositoryId;\n }\n\n async findRepositoryByUrl(url: string): Promise<Repository | undefined> {\n this.logger.debug(`Fetching repository from database by url ${url}...`);\n return await this.knex(this.tableName).where({ url: url }).first();\n }\n\n async deleteRepository(url: string): Promise<void> {\n this.logger.debug(`Deleting repository from database by url ${url}...`);\n const repository = await this.knex(this.tableName)\n .where({ url: url })\n .first();\n\n if (repository) {\n await this.knex(this.tableName).where({ id: repository.id }).del();\n }\n }\n}\n\n// @internal\nexport class ScaffolderTaskDao {\n constructor(private readonly knex: Knex<any, any[]>) {}\n\n async findAllTasks(): Promise<ScaffolderTask[]> {\n return await this.knex('scaffolder_tasks').select<ScaffolderTask[]>(\n 'taskId',\n 'repositoryId',\n 'scaffolderOptions',\n );\n }\n\n async insertTask(task: ScaffolderTask): Promise<string> {\n const result = await this.knex('scaffolder_tasks')\n .insert<ScaffolderTask>({\n taskId: task.taskId,\n repositoryId: task.repositoryId,\n scaffolderOptions: JSON.stringify(task.scaffolderOptions),\n executedAt: task.executedAt,\n })\n .returning<{ taskId: string }[]>('taskId');\n\n return result[0].taskId;\n }\n\n async findTasksByRepositoryId(\n repositoryId: number,\n ): Promise<ScaffolderTask[]> {\n return await this.knex('scaffolder_tasks')\n .where({ repositoryId })\n .select<\n ScaffolderTask[]\n >('taskId', 'repositoryId', 'scaffolderOptions', 'executedAt');\n }\n\n async lastExecutedTaskByRepoId(\n repositoryId: number,\n ): Promise<ScaffolderTask | undefined> {\n return await this.knex('scaffolder_tasks')\n .where({ repositoryId })\n .orderBy('executedAt', 'desc')\n .first<ScaffolderTask>();\n }\n}\n\nexport interface TaskLocation {\n id: number;\n taskId: string;\n location: string;\n type: string;\n}\n\n// @internal\nexport class TaskLocationsDao {\n constructor(private readonly knex: Knex<any, any[]>) {}\n\n async addTaskLocation(\n taskId: string,\n location: string,\n type: string = 'component',\n ): Promise<void> {\n await this.knex('task_locations').insert({ taskId, location, type });\n }\n\n async findLocationsByTaskId(taskId: string): Promise<TaskLocation[]> {\n return await this.knex('task_locations')\n .where({ taskId })\n .select<TaskLocation[]>('id', 'taskId', 'location', 'type');\n }\n\n async findAllLocations(): Promise<TaskLocation[]> {\n return await this.knex('task_locations').select<TaskLocation[]>(\n 'id',\n 'taskId',\n 'location',\n 'type',\n );\n }\n}\n\nexport interface OrchestratorWorkflow {\n id: number;\n instanceId: string;\n repositoryId: number;\n createdAt: Date;\n status?: string;\n}\n\n// @internal\nexport class OrchestratorWorkflowDao {\n constructor(private readonly knex: Knex<any, any[]>) {}\n\n async insertWorkflow(\n instanceId: string,\n repositoryId: number,\n ): Promise<number> {\n const [newWorkflow] = await this.knex('orchestrator_workflows')\n .insert({\n instance_id: instanceId,\n repositoryId: repositoryId,\n })\n .returning('id');\n return newWorkflow.id;\n }\n\n async lastExecutedWorkflowByRepoId(\n repositoryId: number,\n ): Promise<OrchestratorWorkflow | undefined> {\n const result = await this.knex('orchestrator_workflows')\n .select({\n id: 'id',\n instanceId: 'instance_id',\n repositoryId: 'repositoryId',\n createdAt: 'created_at',\n })\n .where({ repositoryId: repositoryId })\n .orderBy('created_at', 'desc')\n .first();\n return result;\n }\n\n async findWorkflowsByRepositoryId(\n repositoryId: number,\n ): Promise<OrchestratorWorkflow[]> {\n return await this.knex('orchestrator_workflows')\n .where({ repositoryId: repositoryId })\n .select({\n id: 'id',\n instanceId: 'instance_id',\n repositoryId: 'repositoryId',\n createdAt: 'created_at',\n });\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"repositoryDao.cjs.js","sources":["../../src/database/repositoryDao.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\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 type { LoggerService } from '@backstage/backend-plugin-api';\n\nimport { Knex } from 'knex';\n\nimport { Components } from '../generated/openapi';\n\nexport interface Repository {\n id: number;\n url: string;\n approvalTool: Components.Schemas.ApprovalTool;\n}\n\nexport interface ScaffolderTask {\n taskId: string;\n scaffolderOptions: any;\n repositoryId: number;\n executedAt?: Date;\n}\n\nexport interface PaginatedResult<T> {\n data: T[];\n page: number;\n size: number;\n total: number;\n totalPages: number;\n}\n\nexport async function paginateQuery<T>(\n queryBuilder: Knex.QueryBuilder<any, T[]>,\n page: number = 1,\n size: number = 10,\n search?: { column: string; term?: string },\n): Promise<PaginatedResult<T>> {\n const offset = (page - 1) * size;\n\n let baseQuery = queryBuilder.clone();\n\n if (search?.term) {\n if (queryBuilder.client.dialect === 'postgres') {\n baseQuery = baseQuery.whereILike(search.column, `%${search.term}%`);\n } else {\n baseQuery = baseQuery.whereLike(search.column, `%${search.term}%`);\n }\n }\n\n const countQuery = baseQuery\n .clone()\n .clearSelect()\n .clearOrder()\n .count<{ count: string }[]>('* as count');\n\n const [rows, [{ count }]] = await Promise.all([\n baseQuery.clone().limit(size).offset(offset),\n countQuery,\n ]);\n\n const total = Number(count);\n\n return {\n data: rows as T[],\n page,\n size,\n total,\n totalPages: Math.ceil(total / size),\n };\n}\n\nexport type RepositoryName = 'repositories' | 'orchestrator_repositories';\n\n// @internal\nexport class RepositoryDao<R extends RepositoryName> {\n constructor(\n private readonly knex: Knex<any, any[]>,\n private readonly logger: LoggerService,\n private readonly tableName: R,\n ) {}\n\n async findRepositories(\n page: number = 1,\n size: number = 10,\n search?: string,\n ): Promise<PaginatedResult<Repository>> {\n this.logger.debug(\n `Fetching repositories page=${page}, size=${size}, search=${search}`,\n );\n const query = this.knex(this.tableName).select('id', 'url', 'approvalTool');\n const searchParam = { column: 'url', term: search };\n return paginateQuery<Repository>(query, page, size, searchParam);\n }\n\n async insertRepository(\n repoUrl: string,\n taskOrWorkflowId: string,\n approvalTool: string,\n ): Promise<number> {\n this.logger.debug(\n `Saving repository ${repoUrl} for task/workflow ${taskOrWorkflowId} to database..`,\n );\n const repository = await this.knex(this.tableName)\n .where({ url: repoUrl })\n .first();\n\n let repositoryId;\n if (repository) {\n repositoryId = repository.id;\n } else {\n const [newRepository] = await this.knex(this.tableName)\n .insert({ url: repoUrl, approvalTool: approvalTool })\n .returning('id');\n repositoryId = newRepository.id;\n }\n return repositoryId;\n }\n\n async findRepositoryByUrl(url: string): Promise<Repository | undefined> {\n this.logger.debug(`Fetching repository from database by url ${url}...`);\n return await this.knex(this.tableName).where({ url: url }).first();\n }\n\n async deleteRepository(url: string): Promise<void> {\n this.logger.debug(`Deleting repository from database by url ${url}...`);\n const repository = await this.knex(this.tableName)\n .where({ url: url })\n .first();\n\n if (repository) {\n await this.knex(this.tableName).where({ id: repository.id }).del();\n }\n }\n}\n\n// @internal\nexport class ScaffolderTaskDao {\n constructor(private readonly knex: Knex<any, any[]>) {}\n\n async findAllTasks(): Promise<ScaffolderTask[]> {\n return await this.knex('scaffolder_tasks').select<ScaffolderTask[]>(\n 'taskId',\n 'repositoryId',\n 'scaffolderOptions',\n );\n }\n\n async insertTask(task: ScaffolderTask): Promise<string> {\n const result = await this.knex('scaffolder_tasks')\n .insert<ScaffolderTask>({\n taskId: task.taskId,\n repositoryId: task.repositoryId,\n scaffolderOptions: JSON.stringify(task.scaffolderOptions),\n executedAt: task.executedAt,\n })\n .returning<{ taskId: string }[]>('taskId');\n\n return result[0].taskId;\n }\n\n async findTasksByRepositoryId(\n repositoryId: number,\n ): Promise<ScaffolderTask[]> {\n return await this.knex('scaffolder_tasks')\n .where({ repositoryId })\n .select<\n ScaffolderTask[]\n >('taskId', 'repositoryId', 'scaffolderOptions', 'executedAt');\n }\n\n async lastExecutedTaskByRepoId(\n repositoryId: number,\n ): Promise<ScaffolderTask | undefined> {\n return await this.knex('scaffolder_tasks')\n .where({ repositoryId })\n .orderBy('executedAt', 'desc')\n .first<ScaffolderTask>();\n }\n}\n\nexport interface TaskLocation {\n id: number;\n taskId: string;\n location: string;\n type: string;\n}\n\n// @internal\nexport class TaskLocationsDao {\n constructor(private readonly knex: Knex<any, any[]>) {}\n\n async addTaskLocation(\n taskId: string,\n location: string,\n type: string = 'component',\n ): Promise<void> {\n await this.knex('task_locations').insert({ taskId, location, type });\n }\n\n async findLocationsByTaskId(taskId: string): Promise<TaskLocation[]> {\n return await this.knex('task_locations')\n .where({ taskId })\n .select<TaskLocation[]>('id', 'taskId', 'location', 'type');\n }\n\n async findAllLocations(): Promise<TaskLocation[]> {\n return await this.knex('task_locations').select<TaskLocation[]>(\n 'id',\n 'taskId',\n 'location',\n 'type',\n );\n }\n}\n\nexport interface OrchestratorWorkflow {\n id: number;\n instanceId: string;\n repositoryId: number;\n createdAt: Date;\n status?: string;\n}\n\n// @internal\nexport class OrchestratorWorkflowDao {\n constructor(private readonly knex: Knex<any, any[]>) {}\n\n async insertWorkflow(\n instanceId: string,\n repositoryId: number,\n ): Promise<number> {\n const [newWorkflow] = await this.knex('orchestrator_workflows')\n .insert({\n instance_id: instanceId,\n repositoryId: repositoryId,\n })\n .returning('id');\n return newWorkflow.id;\n }\n\n async lastExecutedWorkflowByRepoId(\n repositoryId: number,\n ): Promise<OrchestratorWorkflow | undefined> {\n const result = await this.knex('orchestrator_workflows')\n .select({\n id: 'id',\n instanceId: 'instance_id',\n repositoryId: 'repositoryId',\n createdAt: 'created_at',\n })\n .where({ repositoryId: repositoryId })\n .orderBy('created_at', 'desc')\n .first();\n return result;\n }\n\n async findWorkflowsByRepositoryId(\n repositoryId: number,\n ): Promise<OrchestratorWorkflow[]> {\n return await this.knex('orchestrator_workflows')\n .where({ repositoryId: repositoryId })\n .select({\n id: 'id',\n instanceId: 'instance_id',\n repositoryId: 'repositoryId',\n createdAt: 'created_at',\n });\n }\n}\n"],"names":[],"mappings":"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"}
@@ -63,6 +63,37 @@ const OPENAPI = `
63
63
  }
64
64
  }
65
65
  },
66
+ "/scm-hosts": {
67
+ "get": {
68
+ "operationId": "findAllSCMHosts",
69
+ "summary": "Retrieve the SCM Integration hosts",
70
+ "security": [
71
+ {
72
+ "BearerAuth": []
73
+ }
74
+ ],
75
+ "tags": [
76
+ "Management"
77
+ ],
78
+ "responses": {
79
+ "200": {
80
+ "description": "List of Integrations available to the Bulk Import plugin",
81
+ "content": {
82
+ "application/json": {
83
+ "schema": {
84
+ "$ref": "#/components/schemas/SCMHostList"
85
+ },
86
+ "examples": {
87
+ "multipleRepos": {
88
+ "$ref": "#/components/examples/scmHosts"
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ },
66
97
  "/organizations": {
67
98
  "get": {
68
99
  "operationId": "findAllOrganizations",
@@ -165,6 +196,9 @@ const OPENAPI = `
165
196
  },
166
197
  {
167
198
  "$ref": "#/components/parameters/approvalToolParam"
199
+ },
200
+ {
201
+ "$ref": "#/components/parameters/xSCMTokensHeaderParam"
168
202
  }
169
203
  ],
170
204
  "responses": {
@@ -234,6 +268,9 @@ const OPENAPI = `
234
268
  },
235
269
  {
236
270
  "$ref": "#/components/parameters/approvalToolParam"
271
+ },
272
+ {
273
+ "$ref": "#/components/parameters/xSCMTokensHeaderParam"
237
274
  }
238
275
  ],
239
276
  "responses": {
@@ -974,6 +1011,16 @@ const OPENAPI = `
974
1011
  "default": "v1"
975
1012
  }
976
1013
  },
1014
+ "xSCMTokensHeaderParam": {
1015
+ "in": "header",
1016
+ "name": "x-scm-tokens",
1017
+ "description": "Optional JSON-encoded map of SCM host URL to user authentication token. Used to fetch repositories on behalf of the user for each configured SCM host. The value must be a JSON string whose structure matches SCMTokenMap (keys are SCM base URLs, values are OAuth bearer tokens).\\n",
1018
+ "required": false,
1019
+ "schema": {
1020
+ "type": "string",
1021
+ "example": "{\\"https://github.com\\":\\"ghp_xxx\\",\\"https://ghe.example.com\\":\\"ghe_yyy\\"}"
1022
+ }
1023
+ },
977
1024
  "pagePerIntegrationQueryParam": {
978
1025
  "in": "query",
979
1026
  "name": "pagePerIntegration",
@@ -1079,6 +1126,36 @@ const OPENAPI = `
1079
1126
  }
1080
1127
  },
1081
1128
  "schemas": {
1129
+ "SCMTokenMap": {
1130
+ "title": "SCM Token Map",
1131
+ "type": "object",
1132
+ "description": "Map of SCM integration base URL to the user's OAuth access token for that host. Keys must match the base URLs returned by GET /scm-hosts (e.g. https://github.com or https://gitlab.corp.com). Values must be non-empty OAuth bearer tokens scoped to the minimum required access (read-only repository listing). Unknown keys are silently ignored by the server.\\n",
1133
+ "additionalProperties": {
1134
+ "type": "string"
1135
+ },
1136
+ "example": {
1137
+ "https://github.com": "ghp_xxx",
1138
+ "https://ghe.example.com": "ghe_yyy"
1139
+ }
1140
+ },
1141
+ "SCMHostList": {
1142
+ "title": "SCM Host List",
1143
+ "type": "object",
1144
+ "properties": {
1145
+ "github": {
1146
+ "type": "array",
1147
+ "items": {
1148
+ "type": "string"
1149
+ }
1150
+ },
1151
+ "gitlab": {
1152
+ "type": "array",
1153
+ "items": {
1154
+ "type": "string"
1155
+ }
1156
+ }
1157
+ }
1158
+ },
1082
1159
  "OrganizationList": {
1083
1160
  "title": "Organization List",
1084
1161
  "type": "object",
@@ -1538,6 +1615,18 @@ const OPENAPI = `
1538
1615
  }
1539
1616
  },
1540
1617
  "examples": {
1618
+ "scmHosts": {
1619
+ "summary": "Multiple scmHosts",
1620
+ "value": {
1621
+ "github": [
1622
+ "https://github.com",
1623
+ "https://ghe.example.com"
1624
+ ],
1625
+ "gitlab": [
1626
+ "https://gitlab.com"
1627
+ ]
1628
+ }
1629
+ },
1541
1630
  "multipleOrgs": {
1542
1631
  "summary": "Multiple organizations",
1543
1632
  "value": {