Skip to content

Commit 2d8a131

Browse files
Merge pull request #125 from ioBroker/feature/repo-statistics
2 parents 37cf8e2 + 60c6196 commit 2d8a131

File tree

16 files changed

+1386
-249
lines changed

16 files changed

+1386
-249
lines changed

express/backend/package-lock.json

Lines changed: 179 additions & 222 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

express/backend/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,19 @@
1414
"dependencies": {
1515
"@iobroker/create-adapter": "^3.1.2",
1616
"@iobroker/repochecker": "5.0.2",
17-
"@octokit/request": "^5.6.2",
17+
"@octokit/request": "^10.0.7",
1818
"archiver": "^5.3.0",
1919
"axios": "^0.22.0",
2020
"cors": "^2.8.5",
21-
"cron": "^1.8.2",
21+
"cron": "^4.3.4",
2222
"express": "^4.17.1",
2323
"fs-extra": "^10.0.0",
24+
"libsodium-wrappers": "^0.7.15",
25+
"luxon": "~3.7.0",
2426
"mkdirp": "^1.0.4",
2527
"mongodb": "^4.1.3",
2628
"node-cache": "^5.1.2",
2729
"rimraf": "^3.0.2",
28-
"tweetsodium": "0.0.5",
2930
"typescript": "4.4.4",
3031
"universal-cookie-express": "^4.0.3",
3132
"uuid": "^8.3.2",
@@ -37,6 +38,8 @@
3738
"@types/cron": "^1.7.3",
3839
"@types/express": "^4.17.13",
3940
"@types/fs-extra": "^9.0.13",
41+
"@types/libsodium-wrappers": "^0.7.14",
42+
"@types/luxon": "^3.7.1",
4043
"@types/mkdirp": "^1.0.2",
4144
"@types/mongodb": "^4.0.7",
4245
"@types/node": "^16.10.3",
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { Router } from "express";
2+
import { Document } from "mongodb";
3+
import { dbConnect } from "../db/utils";
4+
5+
const router = Router();
6+
7+
const availableFields = {
8+
owner: 1,
9+
10+
// ioPackage fields
11+
tier: "$ioPackage.common.tier",
12+
mode: "$ioPackage.common.mode",
13+
type: "$ioPackage.common.type",
14+
compact: "$ioPackage.common.compact",
15+
connectionType: "$ioPackage.common.connectionType",
16+
dataSource: "$ioPackage.common.dataSource",
17+
adminUiConfig: "$ioPackage.common.adminUI.config",
18+
adminUiTab: "$ioPackage.common.adminUI.tab",
19+
depJsController: "$ioPackage.common.dependencies.js-controller",
20+
depAdmin: "$ioPackage.common.globalDependencies.admin",
21+
22+
// adapter creator fields
23+
target: "$createAdapter.target",
24+
quotes: "$createAdapter.quotes",
25+
indentation: "$createAdapter.indentation",
26+
connectionIndicator: "$createAdapter.connectionIndicator",
27+
language: "$createAdapter.language",
28+
nodeVersion: "$createAdapter.nodeVersion",
29+
creatorVersion: "$createAdapter.creatorVersion",
30+
dependabot: "$createAdapter.dependabot",
31+
releaseScript: "$createAdapter.releaseScript",
32+
33+
// computed fields
34+
publishState: {
35+
$cond: [
36+
{
37+
$gt: [
38+
{
39+
$size: {
40+
$filter: {
41+
input: "$repo-adapters",
42+
as: "ra",
43+
cond: {
44+
$eq: ["$$ra.source", "stable"],
45+
},
46+
},
47+
},
48+
},
49+
0,
50+
],
51+
},
52+
"stable",
53+
{
54+
$cond: [
55+
{
56+
$gt: [
57+
{
58+
$size: {
59+
$filter: {
60+
input: "$repo-adapters",
61+
as: "ra",
62+
cond: {
63+
$eq: ["$$ra.source", "latest"],
64+
},
65+
},
66+
},
67+
},
68+
0,
69+
],
70+
},
71+
"latest",
72+
{
73+
$cond: [
74+
{
75+
$gt: ["$npmMetadata", null],
76+
},
77+
"npm",
78+
"github",
79+
],
80+
},
81+
],
82+
},
83+
],
84+
},
85+
} as const;
86+
87+
router.get("/api/statistics", async function (req, res) {
88+
const db = await dbConnect();
89+
const repos = db.gitHubRepos();
90+
91+
const filters = Object.keys(req.query).filter(
92+
(key) =>
93+
key !== "stats" &&
94+
typeof req.query[key] === "string" &&
95+
availableFields.hasOwnProperty(key),
96+
) as (keyof typeof availableFields)[];
97+
98+
const statsToInclude =
99+
((req.query.stats as string)?.split(
100+
",",
101+
) as (keyof typeof availableFields)[]) ?? [];
102+
statsToInclude.push(...filters);
103+
104+
const pipeline: Document[] = [
105+
{
106+
$match: {
107+
valid: true,
108+
},
109+
},
110+
];
111+
if (statsToInclude.includes("publishState") || req.query.publishState) {
112+
pipeline.push({
113+
$lookup: {
114+
as: "repo-adapters",
115+
from: "repo-adapters",
116+
foreignField: "name",
117+
localField: "adapterName",
118+
},
119+
});
120+
}
121+
122+
const $project: Record<string, any> = {
123+
_id: 0,
124+
};
125+
for (const field in statsToInclude) {
126+
if (availableFields.hasOwnProperty(statsToInclude[field])) {
127+
$project[statsToInclude[field]] =
128+
availableFields[statsToInclude[field]];
129+
}
130+
}
131+
pipeline.push({
132+
$project,
133+
});
134+
135+
if (filters.length) {
136+
const $match: Record<string, any> = {};
137+
for (const field of filters) {
138+
$match[field] = req.query[field];
139+
}
140+
pipeline.push({
141+
$match,
142+
});
143+
}
144+
145+
const stream = repos.aggregate(pipeline).stream();
146+
147+
const stats = new Map<string, Map<string, number>>();
148+
for await (const doc of stream) {
149+
for (const [key, value] of Object.entries(doc)) {
150+
if (req.query[key]) {
151+
// skip stats that are used as filter
152+
continue;
153+
}
154+
if (!stats.has(key)) {
155+
stats.set(key, new Map<string, number>());
156+
}
157+
const valString = String(value);
158+
const stat = stats.get(key)!;
159+
const oldValue = stat.get(valString) || 0;
160+
stat.set(valString, oldValue + 1);
161+
}
162+
}
163+
164+
const result: Record<string, Record<string, number>> = {};
165+
for (const [key, stat] of stats.entries()) {
166+
result[key] = {};
167+
for (const [value, count] of stat.entries()) {
168+
result[key][value] = count;
169+
}
170+
}
171+
172+
res.send(result);
173+
});
174+
175+
export default router;

express/backend/src/apps/create-adapter.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import archiver from "archiver";
1010
import { createHash } from "crypto";
1111
import { Router } from "express";
1212
import { existsSync } from "fs-extra";
13+
import sodium from "libsodium-wrappers";
1314
import mkdirp from "mkdirp";
1415
import path from "path";
1516
import rimraf from "rimraf";
16-
import sodium from "tweetsodium";
1717
import { promisify } from "util";
1818
import { COOKIE_NAME_CREATOR_TOKEN } from "../auth";
1919
import { delay } from "../common";
@@ -230,12 +230,17 @@ export class CreateAdapterConnectionHandler extends WebSocketConnectionHandler<G
230230
},
231231
);
232232

233+
const binKey = sodium.from_base64(
234+
keys.key,
235+
sodium.base64_variants.ORIGINAL,
236+
);
233237
for (const [secretName, value] of Object.entries(secrets)) {
234-
const messageBytes = Buffer.from(value);
235-
const keyBytes = Buffer.from(keys.key, "base64");
236-
const encryptedBytes = sodium.seal(messageBytes, keyBytes);
237-
const encrypted =
238-
Buffer.from(encryptedBytes).toString("base64");
238+
const binSec = sodium.from_string(value);
239+
const encryptedBytes = sodium.crypto_box_seal(binSec, binKey);
240+
const encrypted = sodium.to_base64(
241+
encryptedBytes,
242+
sodium.base64_variants.ORIGINAL,
243+
);
239244

240245
await requestWithAuth(
241246
"PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}",

0 commit comments

Comments
 (0)