@nospt/plugin-tech-radar-ng-backend 0.9.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.
package/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # @nospt/plugin-tech-radar-ng-backend
2
+
3
+ Backend plugin for the **Tech Radar NG** — a Backstage plugin suite that discovers AI/ML technologies, tracks their metrics over time, and exposes a REST API for the frontend to consume.
4
+
5
+ This package provides the **discovery engine**, **metrics ingestion**, **temporal snapshots**, and **REST API** that power the Tech Radar NG.
6
+
7
+ Built with Backstage's [New Backend System](https://backstage.io/docs/backend-system/building-plugins-and-modules/index) using `createBackendPlugin` and Backstage core services (httpRouter, database, scheduler, logger).
8
+
9
+ ---
10
+
11
+ ## Features
12
+
13
+ - **Automated discovery** — searches GitHub for AI/ML repositories on a configurable cron schedule
14
+ - **50/50 discovery strategy** — balances top absolute scorers with fastest-growing emerging repositories
15
+ - **Temporal snapshots** — each discovery run appends timestamped metrics (stars, forks), preserving full history
16
+ - **Metric evolution** — computes percentage change between consecutive snapshots for trend analysis
17
+ - **Human-curated placement** — no ring or segment is ever assigned automatically; the API supports explicit classification by users
18
+ - **Knex + PostgreSQL** — database migrations managed via Knex; compatible with PostgreSQL (production) and SQLite (development)
19
+ - **Extensible architecture** — service-based design with dependency injection for GitHub, HuggingFace (planned), and future data sources
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ From your Backstage root directory:
26
+
27
+ ```bash
28
+ yarn --cwd packages/backend add @nospt/plugin-tech-radar-ng-backend
29
+ ```
30
+
31
+ Register the plugin in `packages/backend/src/index.ts`:
32
+
33
+ ```ts
34
+ const backend = createBackend();
35
+ // ...
36
+ backend.add(import('@nospt/plugin-tech-radar-ng-backend'));
37
+ backend.start();
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Configuration
43
+
44
+ Add the following to your `app-config.yaml`:
45
+
46
+ ```yaml
47
+ techRadarNg:
48
+ discovery:
49
+ cron: '0 8 * * 1' # Every Monday at 08:00 (default)
50
+ github:
51
+ searchQueries:
52
+ - 'AI OR ML OR "machine learning" OR "artificial intelligence"'
53
+ - 'LLM OR "language model" OR GPT OR transformer'
54
+ - '"generative AI" OR "gen AI" OR chatbot OR assistant'
55
+
56
+ backend:
57
+ database:
58
+ client: pg
59
+ connection:
60
+ host: ${POSTGRES_HOST}
61
+ port: ${POSTGRES_PORT}
62
+ user: ${POSTGRES_USER}
63
+ password: ${POSTGRES_PASSWORD}
64
+ database: ${POSTGRES_DB}
65
+ ```
66
+
67
+ | Key | Type | Default | Description |
68
+ | ---------------------------------- | ---------- | ------------- | ---------------------------------------------- |
69
+ | `techRadarNg.discovery.cron` | `string` | `'0 8 * * 1'` | Cron expression for the discovery schedule |
70
+ | `techRadarNg.github.searchQueries` | `string[]` | `[]` | GitHub search queries for repository discovery |
71
+
72
+ ---
73
+
74
+ ## REST API
75
+
76
+ All endpoints are served under `/api/tech-radar-ng`.
77
+
78
+ | Method | Path | Description |
79
+ | ------ | ---------------------- | ------------------------------------------------------ |
80
+ | `GET` | `/segments` | List all radar segments |
81
+ | `GET` | `/rings` | List all radar rings |
82
+ | `GET` | `/candidates` | List candidates (paginated, filterable) |
83
+ | `PUT` | `/candidates/classify` | Batch-update candidate ring, segment, and radar status |
84
+
85
+ ### `GET /candidates` Query Parameters
86
+
87
+ | Parameter | Type | Default | Description |
88
+ | -------------- | --------- | ------------------ | ------------------------------------------------------------------------- |
89
+ | `page` | `integer` | `1` | Page number |
90
+ | `pageSize` | `integer` | `10` | Items per page (max 50) |
91
+ | `unclassified` | `boolean` | `false` | Filter to candidates without ring/segment |
92
+ | `in_radar` | `boolean` | `false` | Filter to candidates on the radar |
93
+ | `orderBy` | `enum` | `popularity_score` | Sort field: `name`, `popularity_score`, `usage_score`, `last_activity_at` |
94
+ | `orderDir` | `enum` | `desc` | Sort direction: `asc`, `desc` |
95
+ | `platforms[]` | `enum[]` | `[]` | Filter by platform: `github` |
96
+
97
+ ### `PUT /candidates/classify` Body
98
+
99
+ A JSON record mapping candidate IDs to patch objects:
100
+
101
+ ```json
102
+ {
103
+ "candidate-uuid-1": {
104
+ "segment": "llm-techniques",
105
+ "ring": "adopt",
106
+ "in_radar": true
107
+ },
108
+ "candidate-uuid-2": { "ring": "hold" }
109
+ }
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Database
115
+
116
+ The plugin uses Knex migrations located in the [`migrations/`](./migrations) directory:
117
+
118
+ | Migration | Description |
119
+ | -------------------------------------------- | ------------------------------------------------------------------- |
120
+ | `001_create_quadrants_table` | Creates the `segments` table |
121
+ | `002_add_quadrants` | Seeds default segment data |
122
+ | `003_create_rings_table` | Creates the `rings` table |
123
+ | `004_add_rings` | Seeds default ring data (Adopt, Trial, Assess, Hold) |
124
+ | `005_create_radar_candidates_table` | Creates the `radar_candidates` table |
125
+ | `006_create_radar_candidates_snapshot_table` | Creates the `radar_candidates_snapshots` table for temporal metrics |
126
+
127
+ Migrations run automatically on plugin startup.
128
+
129
+ ---
130
+
131
+ ## Plugin Structure
132
+
133
+ ```
134
+ src/
135
+ ├── index.ts # Default export (plugin instance)
136
+ ├── plugin.ts # Plugin definition with DI and scheduler
137
+ ├── router.ts # Express router with REST endpoints
138
+ ├── database/
139
+ │ └── TechRadarDb.ts # Database access layer (Knex)
140
+ ├── service/
141
+ │ ├── RadarMetricsService.ts # Core service — orchestrates discovery + metrics
142
+ │ ├── GitHubMetricsService.ts # GitHub API integration
143
+ │ └── HuggingFaceMetricsService.ts # HuggingFace integration (planned)
144
+ └── types/
145
+ ├── index.ts
146
+ ├── techRadar.ts # CandidateRow, CandidateSnapshot
147
+ ├── github.ts # GitHub API response types
148
+ └── huggingFace.ts # HuggingFace API response types
149
+ migrations/
150
+ ├── 001_create_quadrants_table.js
151
+ ├── 002_add_quadrants.js
152
+ ├── 003_create_rings_table.js
153
+ ├── 004_add_rings.js
154
+ ├── 005_create_radar_candidates_table.js
155
+ └── 006_create_radar_candidates_snapshot_table.js
156
+ ```
157
+
158
+ ### Services (Dependency Injection)
159
+
160
+ | Service Ref | Interface | Description |
161
+ | ------------------------------ | --------------------------- | ----------------------------------------------------------------------------- |
162
+ | `radarMetricsServiceRef` | `RadarMetricsService` | Core service — discovery orchestration, metrics retrieval, candidate patching |
163
+ | `gitHubMetricsServiceRef` | `GitHubMetricsService` | Fetches and maps GitHub repositories to candidates |
164
+ | `huggingFaceMetricsServiceRef` | `HuggingFaceMetricsService` | Fetches HuggingFace models (planned) |
165
+ | `techRadarDbRef` | `TechRadarDb` | Database access layer |
166
+
167
+ ---
168
+
169
+ ## Development
170
+
171
+ Start the backend in standalone mode:
172
+
173
+ ```bash
174
+ cd plugins/tech-radar-ng-backend
175
+ yarn start
176
+ ```
177
+
178
+ This uses the dev setup in [`dev/index.ts`](./dev/index.ts) with mocked auth. The API is available at `http://localhost:7007/api/tech-radar-ng/`.
179
+
180
+ To run the full stack (frontend + backend):
181
+
182
+ ```bash
183
+ # From the repository root
184
+ yarn start
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Testing
190
+
191
+ ```bash
192
+ yarn backstage-cli package test # Run tests
193
+ yarn backstage-cli package test --coverage # Run with coverage
194
+ yarn backstage-cli package lint # Lint
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Related Packages
200
+
201
+ | Package | Description |
202
+ | ---------------------------------------------------------------------------- | ------------------------------------ |
203
+ | [`@nospt/plugin-tech-radar-ng`](../tech-radar-ng) | Frontend plugin — Back Office UI |
204
+ | [`@nospt/plugin-tech-radar-ng-common`](../tech-radar-ng-common) | Shared types, Zod schemas, and enums |
@@ -0,0 +1,244 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var errors = require('@backstage/errors');
5
+ var pluginTechRadarNgCommon = require('@nospt/plugin-tech-radar-ng-common');
6
+ var uuid = require('uuid');
7
+
8
+ class DefaultTechRadarDb {
9
+ #logger;
10
+ #database;
11
+ constructor(options) {
12
+ this.#logger = options.logger;
13
+ this.#database = options.database;
14
+ }
15
+ static create(options) {
16
+ return new DefaultTechRadarDb(options);
17
+ }
18
+ async findQuadrants() {
19
+ this.#logger.debug("Fetching segments from the database");
20
+ try {
21
+ const knex = await this.#database.getClient();
22
+ const segments = await knex("segments").select(
23
+ "id",
24
+ "name",
25
+ "search_params"
26
+ );
27
+ this.#logger.debug("Successfully fetched segments from the database");
28
+ return segments.map((q) => ({
29
+ ...q,
30
+ search_params: typeof q.search_params === "string" ? JSON.parse(q.search_params) : q.search_params
31
+ }));
32
+ } catch (error) {
33
+ this.#logger.error(`Error fetching segments: ${errors.stringifyError(error)}`);
34
+ return [];
35
+ }
36
+ }
37
+ async findRings() {
38
+ this.#logger.debug("Fetching rings from the database");
39
+ try {
40
+ const knex = await this.#database.getClient();
41
+ const rings = await knex("rings").select("id", "name", "color");
42
+ this.#logger.debug("Successfully fetched rings from the database");
43
+ return rings;
44
+ } catch (error) {
45
+ this.#logger.error(`Error fetching rings: ${errors.stringifyError(error)}`);
46
+ return [];
47
+ }
48
+ }
49
+ async insertCandidates(candidates) {
50
+ this.#logger.debug(
51
+ `Bulk upserting ${candidates.length} AI radar candidates into the database`
52
+ );
53
+ try {
54
+ const knex = await this.#database.getClient();
55
+ const now = /* @__PURE__ */ new Date();
56
+ const rows = candidates.map((candidate) => ({
57
+ id: uuid.v4(),
58
+ platform_id: candidate.platform_id,
59
+ full_name: candidate.full_name,
60
+ name: candidate.name,
61
+ description: candidate.description,
62
+ url: candidate.url,
63
+ homepage: candidate.homepage,
64
+ primary_language: candidate.primary_language,
65
+ license: candidate.license,
66
+ ring: candidate.ring,
67
+ segment: candidate.segment,
68
+ is_active: candidate.is_active,
69
+ platform: candidate.platform,
70
+ last_activity_at: candidate.last_activity_at,
71
+ created_at: now,
72
+ last_synced_at: now
73
+ }));
74
+ const upsertedIds = await knex("radar_candidates").insert(rows).onConflict("platform_id").merge({
75
+ full_name: knex.raw("EXCLUDED.full_name"),
76
+ name: knex.raw("EXCLUDED.name"),
77
+ url: knex.raw("EXCLUDED.url"),
78
+ description: knex.raw("EXCLUDED.description"),
79
+ homepage: knex.raw("EXCLUDED.homepage"),
80
+ primary_language: knex.raw("EXCLUDED.primary_language"),
81
+ license: knex.raw("EXCLUDED.license"),
82
+ is_active: knex.raw("EXCLUDED.is_active"),
83
+ platform: knex.raw("EXCLUDED.platform"),
84
+ last_synced_at: now
85
+ }).returning(["id", "platform_id"]);
86
+ this.#logger.debug(
87
+ `Successfully bulk upserted ${upsertedIds.length} AI radar candidates`
88
+ );
89
+ return upsertedIds;
90
+ } catch (error) {
91
+ this.#logger.error(
92
+ `Error bulk upserting AI radar candidates: ${errors.stringifyError(error)}`
93
+ );
94
+ return null;
95
+ }
96
+ }
97
+ async insertCandidateSnapshots(candidates) {
98
+ this.#logger.debug(
99
+ `Inserting ${candidates.length} AI radar candidate snapshots into the database`
100
+ );
101
+ try {
102
+ const knex = await this.#database.getClient();
103
+ const rows = candidates.map((candidate) => ({
104
+ id: uuid.v4(),
105
+ candidate_id: candidate.id,
106
+ popularity_score: candidate.popularity_score,
107
+ usage_score: candidate.usage_score,
108
+ snapshot_at: /* @__PURE__ */ new Date()
109
+ }));
110
+ const insertedIds = await knex("radar_candidates_snapshots").insert(rows).returning("id");
111
+ this.#logger.debug(
112
+ `Successfully inserted AI radar candidate snapshots with IDs: ${insertedIds.join(
113
+ ", "
114
+ )}`
115
+ );
116
+ return insertedIds;
117
+ } catch (error) {
118
+ this.#logger.error(
119
+ `Error inserting AI radar candidate snapshots: ${errors.stringifyError(
120
+ error
121
+ )}`
122
+ );
123
+ return null;
124
+ }
125
+ }
126
+ async fetchCandidates(opts) {
127
+ const {
128
+ page,
129
+ pageSize,
130
+ unclassified,
131
+ in_radar,
132
+ orderBy,
133
+ orderDir,
134
+ platforms
135
+ } = opts;
136
+ const offset = (page - 1) * pageSize;
137
+ this.#logger.debug(
138
+ `Fetching candidates page=${page} pageSize=${pageSize} unclassified=${unclassified} in_radar=${in_radar} orderBy=${orderBy} orderDir=${orderDir}`
139
+ );
140
+ try {
141
+ const db = await this.#database.getClient();
142
+ const applyFilters = (qb) => {
143
+ if (platforms && platforms.length > 0) {
144
+ qb.whereIn("c.platform", platforms);
145
+ }
146
+ if (unclassified) {
147
+ qb.where(
148
+ (b) => b.whereNull("c.segment").orWhereNull("c.ring")
149
+ );
150
+ }
151
+ };
152
+ const [countResult] = await db("radar_candidates as c").where("c.is_active", true).andWhere("c.in_radar", in_radar).modify(applyFilters).count("c.id as count");
153
+ const total = Number(countResult.count);
154
+ const orderByColumnMap = /* @__PURE__ */ new Map([
155
+ [pluginTechRadarNgCommon.CandidateOrderBy.NAME, "c.name"],
156
+ [pluginTechRadarNgCommon.CandidateOrderBy.PopularityScore, "popularity_score"],
157
+ [pluginTechRadarNgCommon.CandidateOrderBy.UsageScore, "usage_score"],
158
+ [pluginTechRadarNgCommon.CandidateOrderBy.LastActivityAt, "c.last_activity_at"]
159
+ ]);
160
+ const items = await db("radar_candidates as c").where("c.is_active", true).modify(applyFilters).select([
161
+ "c.id",
162
+ "c.name",
163
+ "c.description",
164
+ "c.url",
165
+ "c.homepage",
166
+ "c.primary_language",
167
+ "c.license",
168
+ "c.ring",
169
+ "c.segment",
170
+ "c.in_radar",
171
+ "c.is_active",
172
+ "c.platform"
173
+ ]).select(
174
+ db.raw(
175
+ "(SELECT popularity_score FROM radar_candidates_snapshots WHERE candidate_id = c.id ORDER BY snapshot_at DESC LIMIT 1) as popularity_score"
176
+ ),
177
+ db.raw(
178
+ "(SELECT usage_score FROM radar_candidates_snapshots WHERE candidate_id = c.id ORDER BY snapshot_at DESC LIMIT 1) as usage_score"
179
+ ),
180
+ db.raw(
181
+ "(SELECT popularity_score FROM radar_candidates_snapshots WHERE candidate_id = c.id ORDER BY snapshot_at DESC LIMIT 1 OFFSET 1) as prev_popularity_score"
182
+ ),
183
+ db.raw(
184
+ "(SELECT usage_score FROM radar_candidates_snapshots WHERE candidate_id = c.id ORDER BY snapshot_at DESC LIMIT 1 OFFSET 1) as prev_usage_score"
185
+ )
186
+ ).orderBy(orderByColumnMap.get(orderBy), orderDir).limit(pageSize).offset(offset);
187
+ this.#logger.debug(
188
+ `Successfully fetched ${items.length} candidates (total: ${total})`
189
+ );
190
+ return {
191
+ items,
192
+ total,
193
+ page,
194
+ pageSize,
195
+ totalPages: Math.ceil(total / pageSize)
196
+ };
197
+ } catch (error) {
198
+ this.#logger.error(`Error fetching candidates: ${errors.stringifyError(error)}`);
199
+ return { items: [], total: 0, page, pageSize, totalPages: 0 };
200
+ }
201
+ }
202
+ async patchCandidates(candidatesPatch) {
203
+ try {
204
+ const db = await this.#database.getClient();
205
+ this.#logger.debug(
206
+ `Patching candidates with updates for ${Object.keys(candidatesPatch).length} candidates`
207
+ );
208
+ await db.transaction(async (trx) => {
209
+ for (const [candidateId, patch] of Object.entries(candidatesPatch)) {
210
+ const updateBody = {};
211
+ if (patch.segment !== void 0) updateBody.segment = patch.segment;
212
+ if (patch.ring !== void 0) updateBody.ring = patch.ring;
213
+ if (patch.in_radar !== void 0)
214
+ updateBody.in_radar = patch.in_radar;
215
+ await trx("radar_candidates").update(updateBody).where({ id: candidateId });
216
+ }
217
+ });
218
+ } catch (dbError) {
219
+ this.#logger.error(
220
+ `Error patching candidates: ${errors.stringifyError(dbError)}`
221
+ );
222
+ throw new errors.ConflictError(
223
+ "Failed to patch candidates due to a conflict. Please try again."
224
+ );
225
+ }
226
+ }
227
+ }
228
+ const techRadarDbRef = backendPluginApi.createServiceRef({
229
+ id: "tech.radar.db",
230
+ defaultFactory: async (service) => backendPluginApi.createServiceFactory({
231
+ service,
232
+ deps: {
233
+ logger: backendPluginApi.coreServices.logger,
234
+ database: backendPluginApi.coreServices.database
235
+ },
236
+ async factory(deps) {
237
+ return DefaultTechRadarDb.create(deps);
238
+ }
239
+ })
240
+ });
241
+
242
+ exports.DefaultTechRadarDb = DefaultTechRadarDb;
243
+ exports.techRadarDbRef = techRadarDbRef;
244
+ //# sourceMappingURL=TechRadarDb.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TechRadarDb.cjs.js","sources":["../../src/database/TechRadarDb.ts"],"sourcesContent":["import {\n coreServices,\n createServiceFactory,\n createServiceRef,\n DatabaseService,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport { ConflictError, stringifyError } from '@backstage/errors';\nimport {\n Segment,\n Ring,\n PaginatedResult,\n CandidatesPatch,\n CandidateQueryValues,\n CandidateOrderBy,\n} from '@nospt/plugin-tech-radar-ng-common';\nimport { CandidateRow } from '../types';\nimport { Knex } from 'knex';\nimport { v4 as uuidv4 } from 'uuid';\n\nexport interface TechRadarDb {\n findQuadrants(): Promise<Segment[]>;\n findRings(): Promise<Ring[]>;\n insertCandidates(\n candidates: CandidateRow[],\n ): Promise<{ id: string; platform_id: string }[] | null>;\n insertCandidateSnapshots(\n candidates: CandidateRow[],\n ): Promise<string[] | null>;\n fetchCandidates(\n opts: CandidateQueryValues,\n ): Promise<PaginatedResult<CandidateRow>>;\n patchCandidates(candidatesPatch: CandidatesPatch): Promise<void>;\n}\n\ninterface TechRadarDbOptions {\n logger: LoggerService;\n database: DatabaseService;\n}\nexport class DefaultTechRadarDb implements TechRadarDb {\n readonly #logger: LoggerService;\n readonly #database: DatabaseService;\n\n private constructor(options: TechRadarDbOptions) {\n this.#logger = options.logger;\n this.#database = options.database;\n }\n\n static create(options: TechRadarDbOptions) {\n return new DefaultTechRadarDb(options);\n }\n\n async findQuadrants(): Promise<Segment[]> {\n this.#logger.debug('Fetching segments from the database');\n try {\n const knex = await this.#database.getClient();\n const segments = await knex('segments').select<Segment[]>(\n 'id',\n 'name',\n 'search_params',\n );\n\n this.#logger.debug('Successfully fetched segments from the database');\n return segments.map(q => ({\n ...q,\n search_params:\n typeof q.search_params === 'string'\n ? JSON.parse(q.search_params)\n : q.search_params,\n }));\n } catch (error) {\n this.#logger.error(`Error fetching segments: ${stringifyError(error)}`);\n return [];\n }\n }\n\n async findRings(): Promise<Ring[]> {\n this.#logger.debug('Fetching rings from the database');\n try {\n const knex = await this.#database.getClient();\n const rings = await knex('rings').select<Ring[]>('id', 'name', 'color');\n\n this.#logger.debug('Successfully fetched rings from the database');\n return rings;\n } catch (error) {\n this.#logger.error(`Error fetching rings: ${stringifyError(error)}`);\n return [];\n }\n }\n\n async insertCandidates(\n candidates: CandidateRow[],\n ): Promise<{ id: string; platform_id: string }[] | null> {\n this.#logger.debug(\n `Bulk upserting ${candidates.length} AI radar candidates into the database`,\n );\n try {\n const knex = await this.#database.getClient();\n const now = new Date();\n const rows = candidates.map(candidate => ({\n id: uuidv4(),\n platform_id: candidate.platform_id,\n full_name: candidate.full_name,\n name: candidate.name,\n description: candidate.description,\n url: candidate.url,\n homepage: candidate.homepage,\n primary_language: candidate.primary_language,\n license: candidate.license,\n ring: candidate.ring,\n segment: candidate.segment,\n is_active: candidate.is_active,\n platform: candidate.platform,\n last_activity_at: candidate.last_activity_at,\n created_at: now,\n last_synced_at: now,\n }));\n\n const upsertedIds = await knex('radar_candidates')\n .insert(rows)\n .onConflict('platform_id')\n .merge({\n full_name: knex.raw('EXCLUDED.full_name'),\n name: knex.raw('EXCLUDED.name'),\n url: knex.raw('EXCLUDED.url'),\n description: knex.raw('EXCLUDED.description'),\n homepage: knex.raw('EXCLUDED.homepage'),\n primary_language: knex.raw('EXCLUDED.primary_language'),\n license: knex.raw('EXCLUDED.license'),\n is_active: knex.raw('EXCLUDED.is_active'),\n platform: knex.raw('EXCLUDED.platform'),\n last_synced_at: now,\n })\n .returning(['id', 'platform_id']);\n\n this.#logger.debug(\n `Successfully bulk upserted ${upsertedIds.length} AI radar candidates`,\n );\n return upsertedIds;\n } catch (error) {\n this.#logger.error(\n `Error bulk upserting AI radar candidates: ${stringifyError(error)}`,\n );\n return null;\n }\n }\n\n async insertCandidateSnapshots(\n candidates: CandidateRow[],\n ): Promise<string[] | null> {\n this.#logger.debug(\n `Inserting ${candidates.length} AI radar candidate snapshots into the database`,\n );\n try {\n const knex = await this.#database.getClient();\n\n const rows = candidates.map(candidate => ({\n id: uuidv4(),\n candidate_id: candidate.id,\n popularity_score: candidate.popularity_score,\n usage_score: candidate.usage_score,\n snapshot_at: new Date(),\n }));\n\n const insertedIds = await knex('radar_candidates_snapshots')\n .insert(rows)\n .returning('id');\n\n this.#logger.debug(\n `Successfully inserted AI radar candidate snapshots with IDs: ${insertedIds.join(\n ', ',\n )}`,\n );\n return insertedIds;\n } catch (error) {\n this.#logger.error(\n `Error inserting AI radar candidate snapshots: ${stringifyError(\n error,\n )}`,\n );\n return null;\n }\n }\n\n async fetchCandidates(\n opts: CandidateQueryValues,\n ): Promise<PaginatedResult<CandidateRow>> {\n const {\n page,\n pageSize,\n unclassified,\n in_radar,\n orderBy,\n orderDir,\n platforms,\n } = opts;\n const offset = (page - 1) * pageSize;\n this.#logger.debug(\n `Fetching candidates page=${page} pageSize=${pageSize} unclassified=${unclassified} in_radar=${in_radar} orderBy=${orderBy} orderDir=${orderDir}`,\n );\n try {\n const db = await this.#database.getClient();\n\n const applyFilters = (qb: Knex.QueryBuilder<CandidateRow, any>) => {\n if (platforms && platforms.length > 0) {\n qb.whereIn('c.platform', platforms);\n }\n\n if (unclassified) {\n qb.where((b: Knex.QueryBuilder<CandidateRow, any>) =>\n b.whereNull('c.segment').orWhereNull('c.ring'),\n );\n }\n };\n\n const [countResult] = await db('radar_candidates as c')\n .where('c.is_active', true)\n .andWhere('c.in_radar', in_radar)\n .modify(applyFilters)\n .count<[{ count: number }]>('c.id as count');\n\n const total = Number(countResult.count);\n\n const orderByColumnMap: Map<CandidateOrderBy, string> = new Map([\n [CandidateOrderBy.NAME, 'c.name'],\n [CandidateOrderBy.PopularityScore, 'popularity_score'],\n [CandidateOrderBy.UsageScore, 'usage_score'],\n [CandidateOrderBy.LastActivityAt, 'c.last_activity_at'],\n ]);\n\n const items = await db('radar_candidates as c')\n .where('c.is_active', true)\n .modify(applyFilters)\n .select<CandidateRow[]>([\n 'c.id',\n 'c.name',\n 'c.description',\n 'c.url',\n 'c.homepage',\n 'c.primary_language',\n 'c.license',\n 'c.ring',\n 'c.segment',\n 'c.in_radar',\n 'c.is_active',\n 'c.platform',\n ])\n .select(\n db.raw(\n '(SELECT popularity_score FROM radar_candidates_snapshots WHERE candidate_id = c.id ORDER BY snapshot_at DESC LIMIT 1) as popularity_score',\n ),\n db.raw(\n '(SELECT usage_score FROM radar_candidates_snapshots WHERE candidate_id = c.id ORDER BY snapshot_at DESC LIMIT 1) as usage_score',\n ),\n db.raw(\n '(SELECT popularity_score FROM radar_candidates_snapshots WHERE candidate_id = c.id ORDER BY snapshot_at DESC LIMIT 1 OFFSET 1) as prev_popularity_score',\n ),\n db.raw(\n '(SELECT usage_score FROM radar_candidates_snapshots WHERE candidate_id = c.id ORDER BY snapshot_at DESC LIMIT 1 OFFSET 1) as prev_usage_score',\n ),\n )\n .orderBy(orderByColumnMap.get(orderBy) as string, orderDir)\n .limit(pageSize)\n .offset(offset);\n\n this.#logger.debug(\n `Successfully fetched ${items.length} candidates (total: ${total})`,\n );\n return {\n items,\n total,\n page,\n pageSize,\n totalPages: Math.ceil(total / pageSize),\n };\n } catch (error) {\n this.#logger.error(`Error fetching candidates: ${stringifyError(error)}`);\n return { items: [], total: 0, page, pageSize, totalPages: 0 };\n }\n }\n\n async patchCandidates(candidatesPatch: CandidatesPatch): Promise<void> {\n try {\n const db = await this.#database.getClient();\n\n this.#logger.debug(\n `Patching candidates with updates for ${\n Object.keys(candidatesPatch).length\n } candidates`,\n );\n\n await db.transaction(async trx => {\n for (const [candidateId, patch] of Object.entries(candidatesPatch)) {\n const updateBody: Partial<{\n segment: string;\n ring: string;\n in_radar: boolean;\n }> = {};\n\n if (patch.segment !== undefined) updateBody.segment = patch.segment;\n if (patch.ring !== undefined) updateBody.ring = patch.ring;\n if (patch.in_radar !== undefined)\n updateBody.in_radar = patch.in_radar;\n\n await trx('radar_candidates')\n .update(updateBody)\n .where({ id: candidateId });\n }\n });\n } catch (dbError) {\n this.#logger.error(\n `Error patching candidates: ${stringifyError(dbError)}`,\n );\n\n throw new ConflictError(\n 'Failed to patch candidates due to a conflict. Please try again.',\n );\n }\n }\n}\n\nexport const techRadarDbRef = createServiceRef<TechRadarDb>({\n id: 'tech.radar.db',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n logger: coreServices.logger,\n database: coreServices.database,\n },\n async factory(deps) {\n return DefaultTechRadarDb.create(deps);\n },\n }),\n});\n"],"names":["stringifyError","uuidv4","CandidateOrderBy","ConflictError","createServiceRef","createServiceFactory","coreServices"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var plugin = require('./plugin.cjs.js');
6
+
7
+
8
+
9
+ exports.default = plugin.techRadarNgPlugin;
10
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -0,0 +1,10 @@
1
+ import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
+
3
+ /**
4
+ * techRadarNgPlugin backend plugin
5
+ *
6
+ * @public
7
+ */
8
+ declare const techRadarNgPlugin: _backstage_backend_plugin_api.BackendFeature;
9
+
10
+ export { techRadarNgPlugin as default };
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var router = require('./router.cjs.js');
5
+ var RadarMetricsService = require('./service/RadarMetricsService.cjs.js');
6
+ var rootHttpRouter = require('@backstage/backend-defaults/rootHttpRouter');
7
+
8
+ const techRadarNgPlugin = backendPluginApi.createBackendPlugin({
9
+ pluginId: "tech-radar-ng",
10
+ register(env) {
11
+ env.registerInit({
12
+ deps: {
13
+ httpRouter: backendPluginApi.coreServices.httpRouter,
14
+ database: backendPluginApi.coreServices.database,
15
+ logger: backendPluginApi.coreServices.logger,
16
+ config: backendPluginApi.coreServices.rootConfig,
17
+ scheduler: backendPluginApi.coreServices.scheduler,
18
+ radarMetricsService: RadarMetricsService.radarMetricsServiceRef
19
+ },
20
+ async init({
21
+ httpRouter,
22
+ database,
23
+ logger,
24
+ config,
25
+ scheduler,
26
+ radarMetricsService
27
+ }) {
28
+ const knex = await database.getClient();
29
+ const migrationsDir = backendPluginApi.resolvePackagePath(
30
+ "@nospt/plugin-tech-radar-ng-backend",
31
+ "migrations"
32
+ );
33
+ logger.info(`Running migrations from: ${migrationsDir}`);
34
+ const [batchNo, log] = await knex.migrate.latest({
35
+ directory: migrationsDir
36
+ });
37
+ logger.info(
38
+ `Migrations complete \u2014 batch ${batchNo}, ran: ${log.join(", ") || "none (already up to date)"} on DB: ${knex.client.config.connection?.database ?? "unknown"}`
39
+ );
40
+ const router$1 = await router.createRouter({
41
+ logger,
42
+ radarMetricsService
43
+ });
44
+ const middleware = rootHttpRouter.MiddlewareFactory.create({ config, logger });
45
+ router$1.use(middleware.notFound());
46
+ router$1.use(middleware.error);
47
+ httpRouter.use(router$1);
48
+ const frequency = config.getOptional("techRadarNg.discovery.frequency") ?? { days: 1 };
49
+ const initialDelay = config.getOptional("techRadarNg.discovery.initialDelay") ?? { minutes: 1 };
50
+ const timeout = config.getOptional("techRadarNg.discovery.timeout") ?? { minutes: 15 };
51
+ await scheduler.scheduleTask({
52
+ id: "tech-radar-ng-weekly-sync",
53
+ frequency,
54
+ timeout,
55
+ initialDelay,
56
+ fn: async () => {
57
+ logger.info("Running Tech Radar Ng sync");
58
+ await radarMetricsService.discover();
59
+ }
60
+ });
61
+ }
62
+ });
63
+ }
64
+ });
65
+
66
+ exports.techRadarNgPlugin = techRadarNgPlugin;
67
+ //# sourceMappingURL=plugin.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["import {\n coreServices,\n createBackendPlugin,\n resolvePackagePath,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './router';\nimport { radarMetricsServiceRef } from './service/RadarMetricsService';\nimport { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter';\nimport { HumanDuration } from '@backstage/types';\n\n/**\n * techRadarNgPlugin backend plugin\n *\n * @public\n */\nexport const techRadarNgPlugin = createBackendPlugin({\n pluginId: 'tech-radar-ng',\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n database: coreServices.database,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n scheduler: coreServices.scheduler,\n radarMetricsService: radarMetricsServiceRef,\n },\n async init({\n httpRouter,\n database,\n logger,\n config,\n scheduler,\n radarMetricsService,\n }) {\n const knex = await database.getClient();\n const migrationsDir = resolvePackagePath(\n '@nospt/plugin-tech-radar-ng-backend',\n 'migrations',\n );\n logger.info(`Running migrations from: ${migrationsDir}`);\n const [batchNo, log] = await knex.migrate.latest({\n directory: migrationsDir,\n });\n logger.info(\n `Migrations complete — batch ${batchNo}, ran: ${\n log.join(', ') || 'none (already up to date)'\n } on DB: ${knex.client.config.connection?.database ?? 'unknown'}`,\n );\n\n const router = await createRouter({\n logger,\n radarMetricsService,\n });\n\n const middleware = MiddlewareFactory.create({ config, logger });\n router.use(middleware.notFound());\n router.use(middleware.error);\n\n httpRouter.use(router);\n\n const frequency: HumanDuration =\n config.getOptional('techRadarNg.discovery.frequency') ?? { days: 1 }; // default to once a day\n\n const initialDelay: HumanDuration = config.getOptional('techRadarNg.discovery.initialDelay') ?? { minutes: 1 };\n const timeout: HumanDuration = config.getOptional('techRadarNg.discovery.timeout') ?? { minutes: 15 };\n\n // Run immediately on startup, then once a week (every Monday at 08:00)\n await scheduler.scheduleTask({\n id: 'tech-radar-ng-weekly-sync',\n frequency,\n timeout,\n initialDelay,\n fn: async () => {\n logger.info('Running Tech Radar Ng sync');\n await radarMetricsService.discover(); // call your service\n },\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","radarMetricsServiceRef","resolvePackagePath","router","createRouter","MiddlewareFactory"],"mappings":";;;;;;;AAeO,MAAM,oBAAoBA,oCAAA,CAAoB;AAAA,EACnD,QAAA,EAAU,eAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,mBAAA,EAAqBC;AAAA,OACvB;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,UAAA;AAAA,QACA,QAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,SAAA;AAAA,QACA;AAAA,OACF,EAAG;AACD,QAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,SAAA,EAAU;AACtC,QAAA,MAAM,aAAA,GAAgBC,mCAAA;AAAA,UACpB,qCAAA;AAAA,UACA;AAAA,SACF;AACA,QAAA,MAAA,CAAO,IAAA,CAAK,CAAA,yBAAA,EAA4B,aAAa,CAAA,CAAE,CAAA;AACvD,QAAA,MAAM,CAAC,OAAA,EAAS,GAAG,IAAI,MAAM,IAAA,CAAK,QAAQ,MAAA,CAAO;AAAA,UAC/C,SAAA,EAAW;AAAA,SACZ,CAAA;AACD,QAAA,MAAA,CAAO,IAAA;AAAA,UACL,CAAA,iCAAA,EAA+B,OAAO,CAAA,OAAA,EACpC,GAAA,CAAI,KAAK,IAAI,CAAA,IAAK,2BACpB,CAAA,QAAA,EAAW,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,YAAY,SAAS,CAAA;AAAA,SACjE;AAEA,QAAA,MAAMC,QAAA,GAAS,MAAMC,mBAAA,CAAa;AAAA,UAChC,MAAA;AAAA,UACA;AAAA,SACD,CAAA;AAED,QAAA,MAAM,aAAaC,gCAAA,CAAkB,MAAA,CAAO,EAAE,MAAA,EAAQ,QAAQ,CAAA;AAC9D,QAAAF,QAAA,CAAO,GAAA,CAAI,UAAA,CAAW,QAAA,EAAU,CAAA;AAC9B,QAAAA,QAAA,CAAO,GAAA,CAAI,WAAW,KAAK,CAAA;AAE7B,QAAA,UAAA,CAAW,IAAIA,QAAM,CAAA;AAErB,QAAA,MAAM,YACJ,MAAA,CAAO,WAAA,CAAY,iCAAiC,CAAA,IAAK,EAAE,MAAM,CAAA,EAAE;AAEnE,QAAA,MAAM,eAA8B,MAAA,CAAO,WAAA,CAAY,oCAAoC,CAAA,IAAK,EAAE,SAAS,CAAA,EAAE;AAC7G,QAAA,MAAM,UAAyB,MAAA,CAAO,WAAA,CAAY,+BAA+B,CAAA,IAAK,EAAE,SAAS,EAAA,EAAG;AAGtG,QAAA,MAAM,UAAU,YAAA,CAAa;AAAA,UAC3B,EAAA,EAAI,2BAAA;AAAA,UACJ,SAAA;AAAA,UACA,OAAA;AAAA,UACA,YAAA;AAAA,UACA,IAAI,YAAY;AACd,YAAA,MAAA,CAAO,KAAK,4BAA4B,CAAA;AACxC,YAAA,MAAM,oBAAoB,QAAA,EAAS;AAAA,UACrC;AAAA,SACD,CAAA;AAAA,MACH;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}