@backstage-community/plugin-tech-insights-backend 0.5.32

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.
@@ -0,0 +1,826 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var backendPluginApi = require('@backstage/backend-plugin-api');
6
+ var pluginTechInsightsNode = require('@backstage-community/plugin-tech-insights-node');
7
+ var catalogClient = require('@backstage/catalog-client');
8
+ var isEmpty = require('lodash/isEmpty');
9
+ var camelCase = require('lodash/camelCase');
10
+ var lodash = require('lodash');
11
+ var backendCommon = require('@backstage/backend-common');
12
+ var semver = require('semver');
13
+ var luxon = require('luxon');
14
+ var catalogModel = require('@backstage/catalog-model');
15
+ var express = require('express');
16
+ var Router = require('express-promise-router');
17
+ var errors = require('@backstage/errors');
18
+ var config = require('@backstage/config');
19
+
20
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
21
+
22
+ var isEmpty__default = /*#__PURE__*/_interopDefaultCompat(isEmpty);
23
+ var camelCase__default = /*#__PURE__*/_interopDefaultCompat(camelCase);
24
+ var express__default = /*#__PURE__*/_interopDefaultCompat(express);
25
+ var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
26
+
27
+ function createFactRetrieverRegistration(options) {
28
+ const { cadence, factRetriever, lifecycle, timeout, initialDelay } = options;
29
+ return {
30
+ cadence,
31
+ factRetriever,
32
+ lifecycle,
33
+ timeout,
34
+ initialDelay
35
+ };
36
+ }
37
+
38
+ const entityMetadataFactRetriever = {
39
+ id: "entityMetadataFactRetriever",
40
+ version: "0.0.1",
41
+ title: "Entity Metadata",
42
+ description: "Generates facts which indicate the completeness of entity metadata",
43
+ schema: {
44
+ hasTitle: {
45
+ type: "boolean",
46
+ description: "The entity has a title in metadata"
47
+ },
48
+ hasDescription: {
49
+ type: "boolean",
50
+ description: "The entity has a description in metadata"
51
+ },
52
+ hasTags: {
53
+ type: "boolean",
54
+ description: "The entity has tags in metadata"
55
+ }
56
+ },
57
+ handler: async ({ discovery, entityFilter, auth }) => {
58
+ const { token } = await auth.getPluginRequestToken({
59
+ onBehalfOf: await auth.getOwnServiceCredentials(),
60
+ targetPluginId: "catalog"
61
+ });
62
+ const catalogClient$1 = new catalogClient.CatalogClient({
63
+ discoveryApi: discovery
64
+ });
65
+ const entities = await catalogClient$1.getEntities(
66
+ { filter: entityFilter },
67
+ { token }
68
+ );
69
+ return entities.items.map((entity) => {
70
+ var _a, _b, _c;
71
+ return {
72
+ entity: {
73
+ namespace: entity.metadata.namespace,
74
+ kind: entity.kind,
75
+ name: entity.metadata.name
76
+ },
77
+ facts: {
78
+ hasTitle: Boolean((_a = entity.metadata) == null ? void 0 : _a.title),
79
+ hasDescription: Boolean((_b = entity.metadata) == null ? void 0 : _b.description),
80
+ hasTags: !isEmpty__default.default((_c = entity.metadata) == null ? void 0 : _c.tags)
81
+ }
82
+ };
83
+ });
84
+ }
85
+ };
86
+
87
+ const entityOwnershipFactRetriever = {
88
+ id: "entityOwnershipFactRetriever",
89
+ version: "0.0.1",
90
+ title: "Entity Ownership",
91
+ description: "Generates facts which indicate the quality of data in the spec.owner field",
92
+ entityFilter: [
93
+ { kind: ["component", "domain", "system", "api", "resource", "template"] }
94
+ ],
95
+ schema: {
96
+ hasOwner: {
97
+ type: "boolean",
98
+ description: "The spec.owner field is set"
99
+ },
100
+ hasGroupOwner: {
101
+ type: "boolean",
102
+ description: "The spec.owner field is set and refers to a group"
103
+ }
104
+ },
105
+ handler: async ({ discovery, entityFilter, auth }) => {
106
+ const { token } = await auth.getPluginRequestToken({
107
+ onBehalfOf: await auth.getOwnServiceCredentials(),
108
+ targetPluginId: "catalog"
109
+ });
110
+ const catalogClient$1 = new catalogClient.CatalogClient({
111
+ discoveryApi: discovery
112
+ });
113
+ const entities = await catalogClient$1.getEntities(
114
+ { filter: entityFilter },
115
+ { token }
116
+ );
117
+ return entities.items.map((entity) => {
118
+ var _a, _b, _c;
119
+ return {
120
+ entity: {
121
+ namespace: entity.metadata.namespace,
122
+ kind: entity.kind,
123
+ name: entity.metadata.name
124
+ },
125
+ facts: {
126
+ hasOwner: Boolean((_a = entity.spec) == null ? void 0 : _a.owner),
127
+ hasGroupOwner: Boolean(
128
+ ((_b = entity.spec) == null ? void 0 : _b.owner) && !((_c = entity.spec) == null ? void 0 : _c.owner).startsWith("user:")
129
+ )
130
+ }
131
+ };
132
+ });
133
+ }
134
+ };
135
+
136
+ const generateAnnotationFactName = (annotation) => camelCase__default.default(`hasAnnotation-${annotation}`);
137
+ const entityHasAnnotation = (entity, annotation) => Boolean(lodash.get(entity, ["metadata", "annotations", annotation]));
138
+ const isTtl = (lifecycle) => {
139
+ return !!lifecycle.timeToLive;
140
+ };
141
+ const isMaxItems = (lifecycle) => {
142
+ return !!lifecycle.maxItems;
143
+ };
144
+
145
+ const techdocsAnnotation = "backstage.io/techdocs-ref";
146
+ const techdocsAnnotationFactName = generateAnnotationFactName(techdocsAnnotation);
147
+ const techdocsFactRetriever = {
148
+ id: "techdocsFactRetriever",
149
+ version: "0.0.1",
150
+ title: "Tech Docs",
151
+ description: "Generates facts related to the completeness of techdocs configuration for entities",
152
+ schema: {
153
+ [techdocsAnnotationFactName]: {
154
+ type: "boolean",
155
+ description: "The entity has a TechDocs reference annotation"
156
+ }
157
+ },
158
+ handler: async ({ discovery, entityFilter, auth }) => {
159
+ const { token } = await auth.getPluginRequestToken({
160
+ onBehalfOf: await auth.getOwnServiceCredentials(),
161
+ targetPluginId: "catalog"
162
+ });
163
+ const catalogClient$1 = new catalogClient.CatalogClient({
164
+ discoveryApi: discovery
165
+ });
166
+ const entities = await catalogClient$1.getEntities(
167
+ { filter: entityFilter },
168
+ { token }
169
+ );
170
+ return entities.items.map((entity) => {
171
+ return {
172
+ entity: {
173
+ namespace: entity.metadata.namespace,
174
+ kind: entity.kind,
175
+ name: entity.metadata.name
176
+ },
177
+ facts: {
178
+ [techdocsAnnotationFactName]: entityHasAnnotation(
179
+ entity,
180
+ techdocsAnnotation
181
+ )
182
+ }
183
+ };
184
+ });
185
+ }
186
+ };
187
+
188
+ var __defProp$1 = Object.defineProperty;
189
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
190
+ var __publicField$1 = (obj, key, value) => {
191
+ __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
192
+ return value;
193
+ };
194
+ class TechInsightsDatabase {
195
+ constructor(db, logger) {
196
+ this.db = db;
197
+ this.logger = logger;
198
+ __publicField$1(this, "CHUNK_SIZE", 50);
199
+ }
200
+ async getLatestSchemas(ids) {
201
+ const queryBuilder = this.db("fact_schemas");
202
+ if (ids) {
203
+ queryBuilder.whereIn("id", ids);
204
+ }
205
+ const existingSchemas = await queryBuilder.orderBy("id", "desc").select();
206
+ const groupedSchemas = lodash.groupBy(existingSchemas, "id");
207
+ return Object.values(groupedSchemas).map((schemas) => {
208
+ const sorted = semver.rsort(schemas.map((it) => it.version));
209
+ return schemas.find((it) => it.version === sorted[0]);
210
+ }).map((it) => ({
211
+ ...lodash.omit(it, "schema"),
212
+ ...JSON.parse(it.schema),
213
+ entityFilter: it.entityFilter ? JSON.parse(it.entityFilter) : null
214
+ }));
215
+ }
216
+ async insertFactSchema(schemaDefinition) {
217
+ const { id, version, schema, entityFilter } = schemaDefinition;
218
+ const existingSchemas = await this.db("fact_schemas").where({ id }).and.where({ version }).select();
219
+ if (!existingSchemas || existingSchemas.length === 0) {
220
+ await this.db("fact_schemas").insert({
221
+ id,
222
+ version,
223
+ entityFilter: entityFilter ? JSON.stringify(entityFilter) : void 0,
224
+ schema: JSON.stringify(schema)
225
+ });
226
+ }
227
+ }
228
+ async insertFacts({
229
+ id,
230
+ facts,
231
+ lifecycle
232
+ }) {
233
+ if (facts.length === 0)
234
+ return;
235
+ const currentSchema = await this.getLatestSchema(id);
236
+ const factRows = facts.map((it) => {
237
+ var _a;
238
+ const ts = (_a = it.timestamp) == null ? void 0 : _a.toISO();
239
+ return {
240
+ id,
241
+ version: currentSchema.version,
242
+ entity: catalogModel.stringifyEntityRef(it.entity),
243
+ facts: JSON.stringify(it.facts),
244
+ ...ts && { timestamp: ts }
245
+ };
246
+ });
247
+ await this.db.transaction(async (tx) => {
248
+ await tx.batchInsert("facts", factRows, this.CHUNK_SIZE);
249
+ if (lifecycle && isTtl(lifecycle)) {
250
+ const expiration = luxon.DateTime.now().minus(lifecycle.timeToLive);
251
+ await this.deleteExpiredFactsByDate(tx, id, expiration);
252
+ }
253
+ if (lifecycle && isMaxItems(lifecycle)) {
254
+ await this.deleteExpiredFactsByNumber(tx, id, lifecycle.maxItems);
255
+ }
256
+ });
257
+ }
258
+ async getLatestFactsByIds(ids, entityTriplet) {
259
+ const results = await this.db("facts").where({ entity: entityTriplet }).and.whereIn("id", ids).join(
260
+ this.db("facts").max("timestamp as maxTimestamp").column("id as subId").where({ entity: entityTriplet }).and.whereIn("id", ids).groupBy("id").as("subQ"),
261
+ {
262
+ "facts.id": "subQ.subId",
263
+ "facts.timestamp": "subQ.maxTimestamp"
264
+ }
265
+ );
266
+ return this.dbFactRowsToTechInsightFacts(results);
267
+ }
268
+ async getFactsBetweenTimestampsByIds(ids, entityTriplet, startDateTime, endDateTime) {
269
+ const results = await this.db("facts").where({ entity: entityTriplet }).and.whereIn("id", ids).and.whereBetween("timestamp", [
270
+ startDateTime.toISO(),
271
+ endDateTime.toISO()
272
+ ]);
273
+ return lodash.groupBy(
274
+ results.map((it) => {
275
+ const { namespace, kind, name } = catalogModel.parseEntityRef(it.entity);
276
+ const timestamp = typeof it.timestamp === "string" ? luxon.DateTime.fromISO(it.timestamp) : luxon.DateTime.fromJSDate(it.timestamp);
277
+ return {
278
+ id: it.id,
279
+ entity: { namespace, kind, name },
280
+ timestamp,
281
+ version: it.version,
282
+ facts: JSON.parse(it.facts)
283
+ };
284
+ }),
285
+ "id"
286
+ );
287
+ }
288
+ async getLatestSchema(id) {
289
+ const existingSchemas = await this.db("fact_schemas").where({ id }).orderBy("id", "desc").select();
290
+ if (existingSchemas.length < 1) {
291
+ this.logger.warn(`No schema found for ${id}. `);
292
+ throw new Error(`No schema found for ${id}. `);
293
+ }
294
+ const sorted = semver.rsort(existingSchemas.map((it) => it.version));
295
+ return existingSchemas.find((it) => it.version === sorted[0]);
296
+ }
297
+ async deleteExpiredFactsByDate(tx, factRetrieverId, timestamp) {
298
+ await tx("facts").where({ id: factRetrieverId }).and.where("timestamp", "<", timestamp.toISO()).delete();
299
+ }
300
+ async deleteExpiredFactsByNumber(tx, factRetrieverId, maxItems) {
301
+ const deletionFilterQuery = (subTx) => subTx.select(["id", "entity", "timestamp"]).from("facts").where({ id: factRetrieverId }).and.whereIn(
302
+ "entity",
303
+ (db) => db.distinct("entity").where({ id: factRetrieverId })
304
+ ).and.leftJoin(
305
+ (joinTable) => joinTable.select("*").from(
306
+ this.db("facts").column(
307
+ { fid: "id" },
308
+ { fentity: "entity" },
309
+ { ftimestamp: "timestamp" }
310
+ ).column(
311
+ this.db.raw(
312
+ "row_number() over (partition by id, entity order by timestamp desc) as fact_rank"
313
+ )
314
+ ).as("ranks")
315
+ ).where("fact_rank", "<=", maxItems).as("filterjoin"),
316
+ (joinClause) => {
317
+ joinClause.on("filterjoin.fid", "facts.id").on("filterjoin.fentity", "facts.entity").on("filterjoin.ftimestamp", "facts.timestamp");
318
+ }
319
+ ).whereNull("filterjoin.fid");
320
+ await tx("facts").whereIn(
321
+ ["id", "entity", "timestamp"],
322
+ (database) => deletionFilterQuery(database)
323
+ ).delete();
324
+ }
325
+ dbFactRowsToTechInsightFacts(rows) {
326
+ return rows.reduce((acc, it) => {
327
+ const { namespace, kind, name } = catalogModel.parseEntityRef(it.entity);
328
+ const timestamp = typeof it.timestamp === "string" ? luxon.DateTime.fromISO(it.timestamp) : luxon.DateTime.fromJSDate(it.timestamp);
329
+ return {
330
+ ...acc,
331
+ [it.id]: {
332
+ id: it.id,
333
+ entity: { namespace, kind, name },
334
+ timestamp,
335
+ version: it.version,
336
+ facts: JSON.parse(it.facts)
337
+ }
338
+ };
339
+ }, {});
340
+ }
341
+ }
342
+
343
+ const migrationsDir = backendCommon.resolvePackagePath(
344
+ "@backstage-community/plugin-tech-insights-backend",
345
+ "migrations"
346
+ );
347
+ const defaultOptions = {
348
+ logger: backendCommon.getVoidLogger()
349
+ };
350
+ const initializePersistenceContext = async (database, options = defaultOptions) => {
351
+ var _a;
352
+ const client = await database.getClient();
353
+ if (!((_a = database.migrations) == null ? void 0 : _a.skip)) {
354
+ await client.migrate.latest({
355
+ directory: migrationsDir
356
+ });
357
+ }
358
+ return {
359
+ techInsightsStore: new TechInsightsDatabase(client, options.logger)
360
+ };
361
+ };
362
+
363
+ async function createRouter(options) {
364
+ const router = Router__default.default();
365
+ router.use(express__default.default.json());
366
+ const { persistenceContext, factChecker, logger } = options;
367
+ const { techInsightsStore } = persistenceContext;
368
+ if (factChecker) {
369
+ logger.info("Fact checker configured. Enabling fact checking endpoints.");
370
+ router.get("/checks", async (_req, res) => {
371
+ return res.json(await factChecker.getChecks());
372
+ });
373
+ router.post("/checks/run/:namespace/:kind/:name", async (req, res) => {
374
+ const { namespace, kind, name } = req.params;
375
+ const { checks } = req.body;
376
+ const entityTriplet = catalogModel.stringifyEntityRef({ namespace, kind, name });
377
+ const checkResult = await factChecker.runChecks(entityTriplet, checks);
378
+ return res.json(checkResult);
379
+ });
380
+ router.post("/checks/run", async (req, res) => {
381
+ const {
382
+ checks,
383
+ entities
384
+ } = req.body;
385
+ const tasks = entities.map(async (entity) => {
386
+ const entityTriplet = typeof entity === "string" ? entity : catalogModel.stringifyEntityRef(entity);
387
+ try {
388
+ const results2 = await factChecker.runChecks(entityTriplet, checks);
389
+ return {
390
+ entity: entityTriplet,
391
+ results: results2
392
+ };
393
+ } catch (e) {
394
+ const error = errors.serializeError(e);
395
+ logger.error(`${error.name}: ${error.message}`);
396
+ return {
397
+ entity: entityTriplet,
398
+ error,
399
+ results: []
400
+ };
401
+ }
402
+ });
403
+ const results = await Promise.all(tasks);
404
+ return res.json(results);
405
+ });
406
+ } else {
407
+ logger.info(
408
+ "Starting tech insights module without fact checking endpoints."
409
+ );
410
+ }
411
+ router.get("/fact-schemas", async (req, res) => {
412
+ const ids = req.query.ids;
413
+ return res.json(await techInsightsStore.getLatestSchemas(ids));
414
+ });
415
+ router.get("/facts/latest", async (req, res) => {
416
+ const { entity } = req.query;
417
+ const { namespace, kind, name } = catalogModel.parseEntityRef(entity);
418
+ if (!req.query.ids) {
419
+ return res.status(422).json({ error: "Failed to parse ids from request" });
420
+ }
421
+ const ids = [req.query.ids].flat();
422
+ return res.json(
423
+ await techInsightsStore.getLatestFactsByIds(
424
+ ids,
425
+ catalogModel.stringifyEntityRef({ namespace, kind, name })
426
+ )
427
+ );
428
+ });
429
+ router.get("/facts/range", async (req, res) => {
430
+ const { entity } = req.query;
431
+ const { namespace, kind, name } = catalogModel.parseEntityRef(entity);
432
+ if (!req.query.ids) {
433
+ return res.status(422).json({ error: "Failed to parse ids from request" });
434
+ }
435
+ const ids = [req.query.ids].flat();
436
+ const startDatetime = luxon.DateTime.fromISO(req.query.startDatetime);
437
+ const endDatetime = luxon.DateTime.fromISO(req.query.endDatetime);
438
+ if (!startDatetime.isValid || !endDatetime.isValid) {
439
+ return res.status(422).json({
440
+ message: "Failed to parse datetime from request",
441
+ field: !startDatetime.isValid ? "startDateTime" : "endDateTime",
442
+ value: !startDatetime.isValid ? startDatetime : endDatetime
443
+ });
444
+ }
445
+ const entityTriplet = catalogModel.stringifyEntityRef({ namespace, kind, name });
446
+ return res.json(
447
+ await techInsightsStore.getFactsBetweenTimestampsByIds(
448
+ ids,
449
+ entityTriplet,
450
+ startDatetime,
451
+ endDatetime
452
+ )
453
+ );
454
+ });
455
+ router.use(backendCommon.errorHandler());
456
+ return router;
457
+ }
458
+
459
+ function randomDailyCron() {
460
+ const rand = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);
461
+ return `${rand(0, 59)} ${rand(0, 23)} * * *`;
462
+ }
463
+ function duration(startTimestamp) {
464
+ const delta = process.hrtime(startTimestamp);
465
+ const seconds = delta[0] + delta[1] / 1e9;
466
+ return `${seconds.toFixed(1)}s`;
467
+ }
468
+ class DefaultFactRetrieverEngine {
469
+ constructor(repository, factRetrieverRegistry, factRetrieverContext, logger, scheduler, defaultCadence, defaultTimeout, defaultInitialDelay) {
470
+ this.repository = repository;
471
+ this.factRetrieverRegistry = factRetrieverRegistry;
472
+ this.factRetrieverContext = factRetrieverContext;
473
+ this.logger = logger;
474
+ this.scheduler = scheduler;
475
+ this.defaultCadence = defaultCadence;
476
+ this.defaultTimeout = defaultTimeout;
477
+ this.defaultInitialDelay = defaultInitialDelay;
478
+ }
479
+ static async create(options) {
480
+ const {
481
+ repository,
482
+ factRetrieverRegistry,
483
+ factRetrieverContext,
484
+ scheduler,
485
+ defaultCadence,
486
+ defaultTimeout,
487
+ defaultInitialDelay
488
+ } = options;
489
+ const retrievers = await factRetrieverRegistry.listRetrievers();
490
+ await Promise.all(retrievers.map((it) => repository.insertFactSchema(it)));
491
+ return new DefaultFactRetrieverEngine(
492
+ repository,
493
+ factRetrieverRegistry,
494
+ factRetrieverContext,
495
+ factRetrieverContext.logger,
496
+ scheduler,
497
+ defaultCadence,
498
+ defaultTimeout,
499
+ defaultInitialDelay
500
+ );
501
+ }
502
+ async schedule() {
503
+ const registrations = await this.factRetrieverRegistry.listRegistrations();
504
+ const newRegs = [];
505
+ await Promise.all(
506
+ registrations.map(async (registration) => {
507
+ const { factRetriever, cadence, lifecycle, timeout, initialDelay } = registration;
508
+ const cronExpression = cadence || this.defaultCadence || randomDailyCron();
509
+ const timeLimit = timeout || this.defaultTimeout || luxon.Duration.fromObject({ minutes: 5 });
510
+ const initialDelaySetting = initialDelay || this.defaultInitialDelay || luxon.Duration.fromObject({ seconds: 5 });
511
+ try {
512
+ await this.scheduler.scheduleTask({
513
+ id: factRetriever.id,
514
+ frequency: { cron: cronExpression },
515
+ fn: this.createFactRetrieverHandler(factRetriever, lifecycle),
516
+ timeout: timeLimit,
517
+ // We add a delay in order to prevent errors due to the
518
+ // fact that the backend is not yet online in a cold-start scenario
519
+ initialDelay: initialDelaySetting
520
+ });
521
+ newRegs.push(factRetriever.id);
522
+ } catch (e) {
523
+ this.logger.warn(
524
+ `Failed to schedule fact retriever ${factRetriever.id}, ${e}`
525
+ );
526
+ }
527
+ })
528
+ );
529
+ this.logger.info(
530
+ `Scheduled ${newRegs.length}/${registrations.length} fact retrievers into the tech-insights engine`
531
+ );
532
+ }
533
+ getJobRegistration(ref) {
534
+ return this.factRetrieverRegistry.get(ref);
535
+ }
536
+ async triggerJob(ref) {
537
+ await this.scheduler.triggerTask(ref);
538
+ }
539
+ createFactRetrieverHandler(factRetriever, lifecycle) {
540
+ return async () => {
541
+ const startTimestamp = process.hrtime();
542
+ this.logger.info(
543
+ `Retrieving facts for fact retriever ${factRetriever.id}`
544
+ );
545
+ let facts = [];
546
+ try {
547
+ facts = await factRetriever.handler({
548
+ ...this.factRetrieverContext,
549
+ logger: this.logger.child({ factRetrieverId: factRetriever.id }),
550
+ entityFilter: factRetriever.entityFilter
551
+ });
552
+ this.logger.debug(
553
+ `Retrieved ${facts.length} facts for fact retriever ${factRetriever.id} in ${duration(startTimestamp)}`
554
+ );
555
+ } catch (e) {
556
+ this.logger.error(
557
+ `Failed to retrieve facts for retriever ${factRetriever.id}`,
558
+ e
559
+ );
560
+ }
561
+ try {
562
+ await this.repository.insertFacts({
563
+ id: factRetriever.id,
564
+ facts,
565
+ lifecycle
566
+ });
567
+ this.logger.info(
568
+ `Stored ${facts.length} facts for fact retriever ${factRetriever.id} in ${duration(startTimestamp)}`
569
+ );
570
+ } catch (e) {
571
+ this.logger.warn(
572
+ `Failed to insert facts for fact retriever ${factRetriever.id}`,
573
+ e
574
+ );
575
+ }
576
+ };
577
+ }
578
+ }
579
+
580
+ var __defProp = Object.defineProperty;
581
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
582
+ var __publicField = (obj, key, value) => {
583
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
584
+ return value;
585
+ };
586
+ class DefaultFactRetrieverRegistry {
587
+ constructor(retrievers) {
588
+ __publicField(this, "retrievers", /* @__PURE__ */ new Map());
589
+ retrievers.forEach((r) => {
590
+ this.registerSync(r);
591
+ });
592
+ }
593
+ registerSync(registration) {
594
+ if (this.retrievers.has(registration.factRetriever.id)) {
595
+ throw new errors.ConflictError(
596
+ `Tech insight fact retriever with identifier '${registration.factRetriever.id}' has already been registered`
597
+ );
598
+ }
599
+ this.retrievers.set(registration.factRetriever.id, registration);
600
+ }
601
+ async register(registration) {
602
+ this.registerSync(registration);
603
+ }
604
+ async get(retrieverReference) {
605
+ const registration = this.retrievers.get(retrieverReference);
606
+ if (!registration) {
607
+ throw new errors.NotFoundError(
608
+ `Tech insight fact retriever with identifier '${retrieverReference}' is not registered.`
609
+ );
610
+ }
611
+ return registration;
612
+ }
613
+ async listRetrievers() {
614
+ return [...this.retrievers.values()].map((it) => it.factRetriever);
615
+ }
616
+ async listRegistrations() {
617
+ return [...this.retrievers.values()];
618
+ }
619
+ async getSchemas() {
620
+ const retrievers = await this.listRetrievers();
621
+ return retrievers.map((it) => it.schema);
622
+ }
623
+ }
624
+
625
+ const buildTechInsightsContext = async (options) => {
626
+ var _a;
627
+ const {
628
+ factRetrievers,
629
+ factCheckerFactory,
630
+ config,
631
+ discovery,
632
+ database,
633
+ logger,
634
+ scheduler,
635
+ tokenManager
636
+ } = options;
637
+ const buildFactRetrieverRegistry = () => {
638
+ if (!options.factRetrieverRegistry) {
639
+ if (!factRetrievers) {
640
+ throw new Error(
641
+ "Failed to build FactRetrieverRegistry because no factRetrievers found"
642
+ );
643
+ }
644
+ return new DefaultFactRetrieverRegistry(factRetrievers);
645
+ }
646
+ return options.factRetrieverRegistry;
647
+ };
648
+ const factRetrieverRegistry = buildFactRetrieverRegistry();
649
+ const persistenceContext = (_a = options.persistenceContext) != null ? _a : await initializePersistenceContext(database, {
650
+ logger
651
+ });
652
+ const { auth } = backendCommon.createLegacyAuthAdapters({
653
+ auth: options.auth,
654
+ tokenManager,
655
+ discovery
656
+ });
657
+ const factRetrieverEngine = await DefaultFactRetrieverEngine.create({
658
+ scheduler,
659
+ repository: persistenceContext.techInsightsStore,
660
+ factRetrieverRegistry,
661
+ factRetrieverContext: {
662
+ config,
663
+ discovery,
664
+ logger,
665
+ tokenManager,
666
+ auth
667
+ }
668
+ });
669
+ await factRetrieverEngine.schedule();
670
+ if (factCheckerFactory) {
671
+ const factChecker = factCheckerFactory.construct(
672
+ persistenceContext.techInsightsStore
673
+ );
674
+ return {
675
+ persistenceContext,
676
+ factChecker,
677
+ factRetrieverEngine
678
+ };
679
+ }
680
+ return {
681
+ persistenceContext,
682
+ factRetrieverEngine
683
+ };
684
+ };
685
+
686
+ function readLifecycleConfig(config$1) {
687
+ if (!config$1) {
688
+ return void 0;
689
+ }
690
+ if (config$1.has("maxItems")) {
691
+ return {
692
+ maxItems: config$1.getNumber("maxItems")
693
+ };
694
+ }
695
+ return {
696
+ timeToLive: config.readDurationFromConfig(config$1.getConfig("timeToLive"))
697
+ };
698
+ }
699
+ function readFactRetrieverConfig(config$1, name) {
700
+ const factRetrieverConfig = config$1.getOptionalConfig(
701
+ `techInsights.factRetrievers.${name}`
702
+ );
703
+ if (!factRetrieverConfig) {
704
+ return void 0;
705
+ }
706
+ const cadence = factRetrieverConfig.getString("cadence");
707
+ const initialDelay = factRetrieverConfig.has("initialDelay") ? config.readDurationFromConfig(factRetrieverConfig.getConfig("initialDelay")) : void 0;
708
+ const lifecycle = readLifecycleConfig(
709
+ factRetrieverConfig.getOptionalConfig("lifecycle")
710
+ );
711
+ const timeout = factRetrieverConfig.has("timeout") ? config.readDurationFromConfig(factRetrieverConfig.getConfig("timeout")) : void 0;
712
+ return {
713
+ cadence,
714
+ initialDelay,
715
+ lifecycle,
716
+ timeout
717
+ };
718
+ }
719
+ function createFactRetrieverRegistrationFromConfig(config, name, factRetriever) {
720
+ const factRetrieverConfig = readFactRetrieverConfig(config, name);
721
+ return factRetrieverConfig ? createFactRetrieverRegistration({
722
+ ...factRetrieverConfig,
723
+ factRetriever
724
+ }) : void 0;
725
+ }
726
+
727
+ const techInsightsPlugin = backendPluginApi.createBackendPlugin({
728
+ pluginId: "tech-insights",
729
+ register(env) {
730
+ let factCheckerFactory = void 0;
731
+ env.registerExtensionPoint(pluginTechInsightsNode.techInsightsFactCheckerFactoryExtensionPoint, {
732
+ setFactCheckerFactory(factory) {
733
+ factCheckerFactory = factory;
734
+ }
735
+ });
736
+ let factRetrieverRegistry = void 0;
737
+ env.registerExtensionPoint(
738
+ pluginTechInsightsNode.techInsightsFactRetrieverRegistryExtensionPoint,
739
+ {
740
+ setFactRetrieverRegistry(registry) {
741
+ factRetrieverRegistry = registry;
742
+ }
743
+ }
744
+ );
745
+ const addedFactRetrievers = {
746
+ entityMetadataFactRetriever,
747
+ entityOwnershipFactRetriever,
748
+ techdocsFactRetriever
749
+ };
750
+ env.registerExtensionPoint(pluginTechInsightsNode.techInsightsFactRetrieversExtensionPoint, {
751
+ addFactRetrievers(factRetrievers) {
752
+ Object.entries(factRetrievers).forEach(([key, value]) => {
753
+ addedFactRetrievers[key] = value;
754
+ });
755
+ }
756
+ });
757
+ let persistenceContext = void 0;
758
+ env.registerExtensionPoint(pluginTechInsightsNode.techInsightsPersistenceContextExtensionPoint, {
759
+ setPersistenceContext(context) {
760
+ persistenceContext = context;
761
+ }
762
+ });
763
+ env.registerInit({
764
+ deps: {
765
+ config: backendPluginApi.coreServices.rootConfig,
766
+ database: backendPluginApi.coreServices.database,
767
+ discovery: backendPluginApi.coreServices.discovery,
768
+ httpRouter: backendPluginApi.coreServices.httpRouter,
769
+ logger: backendPluginApi.coreServices.logger,
770
+ scheduler: backendPluginApi.coreServices.scheduler,
771
+ tokenManager: backendPluginApi.coreServices.tokenManager,
772
+ auth: backendPluginApi.coreServices.auth
773
+ },
774
+ async init({
775
+ config,
776
+ database,
777
+ discovery,
778
+ httpRouter,
779
+ logger,
780
+ scheduler,
781
+ tokenManager,
782
+ auth
783
+ }) {
784
+ const factRetrievers = Object.entries(
785
+ addedFactRetrievers
786
+ ).map(
787
+ ([name, factRetriever]) => createFactRetrieverRegistrationFromConfig(
788
+ config,
789
+ name,
790
+ factRetriever
791
+ )
792
+ ).filter((registration) => registration);
793
+ const context = await buildTechInsightsContext({
794
+ config,
795
+ database,
796
+ discovery,
797
+ factCheckerFactory,
798
+ factRetrieverRegistry,
799
+ factRetrievers,
800
+ logger,
801
+ persistenceContext,
802
+ scheduler,
803
+ tokenManager,
804
+ auth
805
+ });
806
+ httpRouter.use(
807
+ await createRouter({
808
+ ...context,
809
+ config,
810
+ logger
811
+ })
812
+ );
813
+ }
814
+ });
815
+ }
816
+ });
817
+
818
+ exports.buildTechInsightsContext = buildTechInsightsContext;
819
+ exports.createFactRetrieverRegistration = createFactRetrieverRegistration;
820
+ exports.createRouter = createRouter;
821
+ exports.default = techInsightsPlugin;
822
+ exports.entityMetadataFactRetriever = entityMetadataFactRetriever;
823
+ exports.entityOwnershipFactRetriever = entityOwnershipFactRetriever;
824
+ exports.initializePersistenceContext = initializePersistenceContext;
825
+ exports.techdocsFactRetriever = techdocsFactRetriever;
826
+ //# sourceMappingURL=index.cjs.js.map