@backstage-community/plugin-tech-insights-backend 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/index.cjs.js +19 -815
- package/dist/index.cjs.js.map +1 -1
- package/dist/plugin/config.cjs.js +59 -0
- package/dist/plugin/config.cjs.js.map +1 -0
- package/dist/plugin/plugin.cjs.js +102 -0
- package/dist/plugin/plugin.cjs.js.map +1 -0
- package/dist/service/fact/FactRetrieverEngine.cjs.js +127 -0
- package/dist/service/fact/FactRetrieverEngine.cjs.js.map +1 -0
- package/dist/service/fact/FactRetrieverRegistry.cjs.js +45 -0
- package/dist/service/fact/FactRetrieverRegistry.cjs.js.map +1 -0
- package/dist/service/fact/createFactRetriever.cjs.js +15 -0
- package/dist/service/fact/createFactRetriever.cjs.js.map +1 -0
- package/dist/service/fact/factRetrievers/entityMetadataFactRetriever.cjs.js +59 -0
- package/dist/service/fact/factRetrievers/entityMetadataFactRetriever.cjs.js.map +1 -0
- package/dist/service/fact/factRetrievers/entityOwnershipFactRetriever.cjs.js +54 -0
- package/dist/service/fact/factRetrievers/entityOwnershipFactRetriever.cjs.js.map +1 -0
- package/dist/service/fact/factRetrievers/techdocsFactRetriever.cjs.js +62 -0
- package/dist/service/fact/factRetrievers/techdocsFactRetriever.cjs.js.map +1 -0
- package/dist/service/fact/factRetrievers/utils.cjs.js +23 -0
- package/dist/service/fact/factRetrievers/utils.cjs.js.map +1 -0
- package/dist/service/persistence/TechInsightsDatabase.cjs.js +161 -0
- package/dist/service/persistence/TechInsightsDatabase.cjs.js.map +1 -0
- package/dist/service/persistence/persistenceContext.cjs.js +23 -0
- package/dist/service/persistence/persistenceContext.cjs.js.map +1 -0
- package/dist/service/router.cjs.js +120 -0
- package/dist/service/router.cjs.js.map +1 -0
- package/dist/service/techInsightsContextBuilder.cjs.js +62 -0
- package/dist/service/techInsightsContextBuilder.cjs.js.map +1 -0
- 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
|
|
6
|
-
var
|
|
7
|
-
var
|
|
8
|
-
var
|
|
9
|
-
var
|
|
10
|
-
var
|
|
11
|
-
var
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|