@backstage-community/plugin-tech-insights-backend 1.2.0 → 1.2.2

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