verdaccio-stats 0.3.1 → 0.3.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/README.md +7 -0
- package/lib/config.d.ts +2 -2
- package/lib/debugger.d.ts +1 -1
- package/lib/index.js +188 -144
- package/lib/index.mjs +189 -145
- package/lib/middlewares/hooks.d.ts +1 -1
- package/lib/middlewares/stats.d.ts +1 -1
- package/lib/migrations.d.ts +1 -1
- package/lib/models.d.ts +39 -13
- package/lib/storage/db.d.ts +3 -3
- package/lib/utils.d.ts +10 -2
- package/package.json +22 -22
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
This plugin adds detailed statistics functionality to your Verdaccio private npm registry.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/verdaccio-stats)
|
|
6
|
+
[](https://www.npmjs.com/package/verdaccio-stats)
|
|
7
|
+
[](https://www.npmjs.com/package/verdaccio-stats)
|
|
8
|
+
[](https://www.npmjs.com/package/verdaccio-stats)
|
|
9
|
+
|
|
5
10
|
## Features
|
|
6
11
|
|
|
7
12
|
- Track package download statistics
|
|
@@ -51,6 +56,8 @@ middlewares:
|
|
|
51
56
|
count-manifest-views: true # Optional, whether to count manifest views
|
|
52
57
|
```
|
|
53
58
|
|
|
59
|
+
Note: SQLite is the default database type, but for performance reason, it is not recommended for production use. For production, consider using MySQL, PostgreSQL, MariaDB, or MSSQL.
|
|
60
|
+
|
|
54
61
|
### Configuration Options
|
|
55
62
|
|
|
56
63
|
| Option | Type | Default | Description |
|
package/lib/config.d.ts
CHANGED
|
@@ -86,7 +86,9 @@ export interface ConfigHolder {
|
|
|
86
86
|
export type StatsConfig = z.infer<typeof statsConfig>;
|
|
87
87
|
export declare class ParsedPluginConfig implements ConfigHolder {
|
|
88
88
|
private readonly verdaccioConfig;
|
|
89
|
+
private config;
|
|
89
90
|
readonly favicon: string;
|
|
91
|
+
constructor(config: StatsConfig, verdaccioConfig: Config);
|
|
90
92
|
get configPath(): string;
|
|
91
93
|
get countDownloads(): boolean;
|
|
92
94
|
get countManifestViews(): boolean;
|
|
@@ -94,7 +96,5 @@ export declare class ParsedPluginConfig implements ConfigHolder {
|
|
|
94
96
|
get logo(): string | undefined;
|
|
95
97
|
get sequelizeOptions(): SequelizeOptions;
|
|
96
98
|
get title(): string;
|
|
97
|
-
private config;
|
|
98
|
-
constructor(config: StatsConfig, verdaccioConfig: Config);
|
|
99
99
|
}
|
|
100
100
|
export {};
|
package/lib/debugger.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
5
|
var z = require('zod');
|
|
6
|
+
var path = require('node:path');
|
|
6
7
|
var dayjs = require('dayjs');
|
|
7
8
|
var advancedFormat = require('dayjs/plugin/advancedFormat');
|
|
8
9
|
var isoWeek = require('dayjs/plugin/isoWeek');
|
|
9
10
|
var weekOfYear = require('dayjs/plugin/weekOfYear');
|
|
10
11
|
var weekYear = require('dayjs/plugin/weekYear');
|
|
11
|
-
var
|
|
12
|
+
var buildDebug = require('debug');
|
|
12
13
|
var core = require('@verdaccio/core');
|
|
13
14
|
var sequelize = require('sequelize');
|
|
14
|
-
var buildDebug = require('debug');
|
|
15
15
|
var umzug = require('umzug');
|
|
16
16
|
|
|
17
17
|
var name = "verdaccio-stats";
|
|
18
|
-
var version = "0.3.
|
|
18
|
+
var version = "0.3.3";
|
|
19
19
|
|
|
20
20
|
const plugin = {
|
|
21
21
|
name,
|
|
@@ -102,6 +102,16 @@ function getPeriodValue(periodType, date, isoWeek) {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Get the period value for the current date.
|
|
107
|
+
*
|
|
108
|
+
* @param periodType {PeriodType} - The period type.
|
|
109
|
+
* @param isoWeek {boolean} - Whether to use ISO week format.
|
|
110
|
+
* @returns {PeriodValue} The period value for the current date.
|
|
111
|
+
*/
|
|
112
|
+
function getCurrentPeriodValue(periodType, isoWeek) {
|
|
113
|
+
return getPeriodValue(periodType, new Date(), isoWeek);
|
|
114
|
+
}
|
|
105
115
|
/**
|
|
106
116
|
* Check if the status code is a success status code.
|
|
107
117
|
*
|
|
@@ -210,7 +220,19 @@ const statsConfig = z
|
|
|
210
220
|
});
|
|
211
221
|
class ParsedPluginConfig {
|
|
212
222
|
verdaccioConfig;
|
|
223
|
+
config;
|
|
213
224
|
favicon = "/-/static/favicon.ico";
|
|
225
|
+
constructor(config, verdaccioConfig) {
|
|
226
|
+
this.verdaccioConfig = verdaccioConfig;
|
|
227
|
+
try {
|
|
228
|
+
this.config = statsConfig.parse(config);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
const fieldErrors = err.flatten().fieldErrors;
|
|
232
|
+
logger.error({ errors: fieldErrors }, "Invalid config for verdaccio stats plugin, @{errors}");
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
214
236
|
get configPath() {
|
|
215
237
|
return this.verdaccioConfig.configPath ?? this.verdaccioConfig.self_path;
|
|
216
238
|
}
|
|
@@ -251,18 +273,16 @@ class ParsedPluginConfig {
|
|
|
251
273
|
get title() {
|
|
252
274
|
return this.verdaccioConfig.web?.title ? `${this.verdaccioConfig.web.title} - Stats` : "Verdaccio Stats";
|
|
253
275
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const debug = buildDebug(`verdaccio:plugin:${pluginKey}`);
|
|
279
|
+
function getUmzugLogger() {
|
|
280
|
+
return {
|
|
281
|
+
debug: (msg) => debug("umzug:debug: %j", msg),
|
|
282
|
+
error: (msg) => debug("umzug:error: %j", msg),
|
|
283
|
+
info: (msg) => debug("umzug:info: %j", msg),
|
|
284
|
+
warn: (msg) => debug("umzug:warn: %j", msg),
|
|
285
|
+
};
|
|
266
286
|
}
|
|
267
287
|
|
|
268
288
|
class Hooks {
|
|
@@ -355,9 +375,11 @@ class Stats {
|
|
|
355
375
|
};
|
|
356
376
|
}
|
|
357
377
|
|
|
358
|
-
class
|
|
378
|
+
class StatsModel extends sequelize.Model {
|
|
359
379
|
}
|
|
360
|
-
class
|
|
380
|
+
class DownloadStats extends StatsModel {
|
|
381
|
+
}
|
|
382
|
+
class ManifestViewStats extends StatsModel {
|
|
361
383
|
}
|
|
362
384
|
class Package extends sequelize.Model {
|
|
363
385
|
}
|
|
@@ -404,7 +426,18 @@ class UI {
|
|
|
404
426
|
{
|
|
405
427
|
resource: Package,
|
|
406
428
|
options: {
|
|
407
|
-
actions: {
|
|
429
|
+
actions: {
|
|
430
|
+
...defaultActions,
|
|
431
|
+
search: {
|
|
432
|
+
before: (request) => {
|
|
433
|
+
if (request.params.action !== "search") {
|
|
434
|
+
return request;
|
|
435
|
+
}
|
|
436
|
+
request.query = { ...request.query, searchProperty: "name" };
|
|
437
|
+
return request;
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
},
|
|
408
441
|
titleProperty: "displayName",
|
|
409
442
|
listProperties: ["id", "name", "version", "createdAt"],
|
|
410
443
|
showProperties: ["id", "name", "version", "createdAt"],
|
|
@@ -412,38 +445,36 @@ class UI {
|
|
|
412
445
|
sort: { sortBy: "createdAt", direction: "desc" },
|
|
413
446
|
},
|
|
414
447
|
},
|
|
415
|
-
this.config.countDownloads &&
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
availableValues: PERIOD_TYPES.map((type) => ({ value: type, label: type })),
|
|
423
|
-
},
|
|
448
|
+
this.config.countDownloads && {
|
|
449
|
+
resource: DownloadStats,
|
|
450
|
+
options: {
|
|
451
|
+
actions: { ...defaultActions },
|
|
452
|
+
properties: {
|
|
453
|
+
periodType: {
|
|
454
|
+
availableValues: PERIOD_TYPES.map((type) => ({ value: type, label: type })),
|
|
424
455
|
},
|
|
425
|
-
listProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
426
|
-
showProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
427
|
-
filterProperties: ["packageId", "periodType", "periodValue", "createdAt", "updatedAt"],
|
|
428
|
-
sort: { sortBy: "updatedAt", direction: "desc" },
|
|
429
456
|
},
|
|
457
|
+
listProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
458
|
+
showProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
459
|
+
filterProperties: ["packageId", "periodType", "periodValue", "createdAt", "updatedAt"],
|
|
460
|
+
sort: { sortBy: "updatedAt", direction: "desc" },
|
|
430
461
|
},
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
},
|
|
462
|
+
},
|
|
463
|
+
this.config.countManifestViews && {
|
|
464
|
+
resource: ManifestViewStats,
|
|
465
|
+
options: {
|
|
466
|
+
actions: { ...defaultActions },
|
|
467
|
+
properties: {
|
|
468
|
+
periodType: {
|
|
469
|
+
availableValues: PERIOD_TYPES.map((type) => ({ value: type, label: type })),
|
|
440
470
|
},
|
|
441
|
-
listProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
442
|
-
showProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
443
|
-
filterProperties: ["packageId", "periodType", "periodValue", "createdAt", "updatedAt"],
|
|
444
|
-
sort: { sortBy: "updatedAt", direction: "desc" },
|
|
445
471
|
},
|
|
472
|
+
listProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
473
|
+
showProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
474
|
+
filterProperties: ["packageId", "periodType", "periodValue", "createdAt", "updatedAt"],
|
|
475
|
+
sort: { sortBy: "updatedAt", direction: "desc" },
|
|
446
476
|
},
|
|
477
|
+
},
|
|
447
478
|
].filter(Boolean),
|
|
448
479
|
rootPath: rootPath,
|
|
449
480
|
branding: {
|
|
@@ -459,16 +490,6 @@ class UI {
|
|
|
459
490
|
}
|
|
460
491
|
}
|
|
461
492
|
|
|
462
|
-
const debug = buildDebug(`verdaccio:plugin:${pluginKey}`);
|
|
463
|
-
function getUmzugLogger() {
|
|
464
|
-
return {
|
|
465
|
-
debug: (msg) => debug("umzug:debug: %j", msg),
|
|
466
|
-
error: (msg) => debug("umzug:error: %j", msg),
|
|
467
|
-
info: (msg) => debug("umzug:info: %j", msg),
|
|
468
|
-
warn: (msg) => debug("umzug:warn: %j", msg),
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
493
|
const migrations = [
|
|
473
494
|
{
|
|
474
495
|
name: "000-initial",
|
|
@@ -538,15 +559,16 @@ class Database {
|
|
|
538
559
|
config;
|
|
539
560
|
sequelize;
|
|
540
561
|
umzug;
|
|
562
|
+
universePackage = null;
|
|
541
563
|
constructor(config) {
|
|
542
564
|
const sequelizeOptions = config.sequelizeOptions;
|
|
543
565
|
logger.debug({ dialect: sequelizeOptions.dialect }, "Creating @{dialect} database connection");
|
|
544
566
|
const sequelize$1 = new sequelize.Sequelize({
|
|
545
567
|
...sequelizeOptions,
|
|
546
|
-
logging: (sql) =>
|
|
568
|
+
logging: (sql) => debug(sql),
|
|
547
569
|
pool: {
|
|
548
|
-
max:
|
|
549
|
-
min:
|
|
570
|
+
max: 10,
|
|
571
|
+
min: 2,
|
|
550
572
|
acquire: 30_000,
|
|
551
573
|
idle: 10_000,
|
|
552
574
|
},
|
|
@@ -565,6 +587,60 @@ class Database {
|
|
|
565
587
|
this.umzug = umzug$1;
|
|
566
588
|
this.init();
|
|
567
589
|
}
|
|
590
|
+
init() {
|
|
591
|
+
Package.init({
|
|
592
|
+
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: sequelize.DataTypes.INTEGER },
|
|
593
|
+
name: { allowNull: false, type: sequelize.DataTypes.STRING(100) },
|
|
594
|
+
version: { allowNull: false, type: sequelize.DataTypes.STRING(50) },
|
|
595
|
+
displayName: {
|
|
596
|
+
type: sequelize.DataTypes.VIRTUAL(sequelize.DataTypes.STRING, ["name", "version"]),
|
|
597
|
+
get() {
|
|
598
|
+
return `${this.getDataValue("name")}@${this.getDataValue("version")}`;
|
|
599
|
+
},
|
|
600
|
+
set() {
|
|
601
|
+
throw new Error("Virtual property, cannot be set");
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
}, { sequelize: this.sequelize, tableName: "packages", underscored: true });
|
|
605
|
+
DownloadStats.init({
|
|
606
|
+
count: { allowNull: false, type: sequelize.DataTypes.BIGINT, defaultValue: 0 },
|
|
607
|
+
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: sequelize.DataTypes.INTEGER },
|
|
608
|
+
packageId: { allowNull: false, type: sequelize.DataTypes.INTEGER, references: { model: Package, key: "id" } },
|
|
609
|
+
periodType: { allowNull: false, type: sequelize.DataTypes.ENUM(...PERIOD_TYPES), values: PERIOD_TYPES },
|
|
610
|
+
periodValue: { allowNull: false, type: sequelize.DataTypes.STRING(20) },
|
|
611
|
+
}, {
|
|
612
|
+
sequelize: this.sequelize,
|
|
613
|
+
tableName: "download_stats",
|
|
614
|
+
underscored: true,
|
|
615
|
+
});
|
|
616
|
+
ManifestViewStats.init({
|
|
617
|
+
count: { allowNull: false, type: sequelize.DataTypes.BIGINT, defaultValue: 0 },
|
|
618
|
+
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: sequelize.DataTypes.INTEGER },
|
|
619
|
+
packageId: { allowNull: false, type: sequelize.DataTypes.INTEGER, references: { model: Package, key: "id" } },
|
|
620
|
+
periodType: { allowNull: false, type: sequelize.DataTypes.ENUM(...PERIOD_TYPES), values: PERIOD_TYPES },
|
|
621
|
+
periodValue: { allowNull: false, type: sequelize.DataTypes.STRING(20) },
|
|
622
|
+
}, { sequelize: this.sequelize, tableName: "manifest_view_stats", underscored: true });
|
|
623
|
+
Package.hasMany(DownloadStats, {
|
|
624
|
+
sourceKey: "id",
|
|
625
|
+
foreignKey: "packageId",
|
|
626
|
+
as: "downloadStats",
|
|
627
|
+
});
|
|
628
|
+
Package.hasMany(ManifestViewStats, {
|
|
629
|
+
sourceKey: "id",
|
|
630
|
+
foreignKey: "packageId",
|
|
631
|
+
as: "manifestViewStats",
|
|
632
|
+
});
|
|
633
|
+
DownloadStats.belongsTo(Package, {
|
|
634
|
+
targetKey: "id",
|
|
635
|
+
foreignKey: "packageId",
|
|
636
|
+
as: "package",
|
|
637
|
+
});
|
|
638
|
+
ManifestViewStats.belongsTo(Package, {
|
|
639
|
+
targetKey: "id",
|
|
640
|
+
foreignKey: "packageId",
|
|
641
|
+
as: "package",
|
|
642
|
+
});
|
|
643
|
+
}
|
|
568
644
|
async addDownloadCount(packageName, version) {
|
|
569
645
|
const t = await this.sequelize.transaction();
|
|
570
646
|
try {
|
|
@@ -607,49 +683,51 @@ class Database {
|
|
|
607
683
|
rollback() {
|
|
608
684
|
return this.umzug.down();
|
|
609
685
|
}
|
|
610
|
-
async
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
packageId: pkg.id,
|
|
615
|
-
periodType,
|
|
616
|
-
periodValue: getPeriodValue(periodType, undefined, this.config.isoWeek),
|
|
617
|
-
},
|
|
618
|
-
transaction,
|
|
619
|
-
});
|
|
620
|
-
return downloadStats;
|
|
621
|
-
}));
|
|
622
|
-
return Promise.all(downloadStatsList.map((downloadStats) => downloadStats.increment("count", { by: 1, transaction })));
|
|
623
|
-
}
|
|
624
|
-
async addManifestViewForAllPeriod(pkg, transaction) {
|
|
625
|
-
const manifestViewStatsList = await Promise.all(PERIOD_TYPES.map(async (periodType) => {
|
|
626
|
-
const [manifestViewStats] = await ManifestViewStats.findOrCreate({
|
|
627
|
-
where: {
|
|
628
|
-
packageId: pkg.id,
|
|
629
|
-
periodType,
|
|
630
|
-
periodValue: getPeriodValue(periodType, undefined, this.config.isoWeek),
|
|
631
|
-
},
|
|
632
|
-
transaction,
|
|
633
|
-
});
|
|
634
|
-
return manifestViewStats;
|
|
686
|
+
async addStatsForAllPeriod(pkg, statsModel, transaction) {
|
|
687
|
+
const periodValues = PERIOD_TYPES.map((periodType) => ({
|
|
688
|
+
periodType,
|
|
689
|
+
periodValue: getCurrentPeriodValue(periodType, this.config.isoWeek),
|
|
635
690
|
}));
|
|
636
|
-
|
|
691
|
+
const existingStats = await statsModel.findAll({
|
|
692
|
+
where: {
|
|
693
|
+
packageId: pkg.id,
|
|
694
|
+
[sequelize.Op.or]: periodValues,
|
|
695
|
+
},
|
|
696
|
+
transaction,
|
|
697
|
+
});
|
|
698
|
+
const statsToCreate = [];
|
|
699
|
+
const statsIdsToUpdate = [];
|
|
700
|
+
for (const { periodType, periodValue } of periodValues) {
|
|
701
|
+
const existingStat = existingStats.find((stat) => stat.periodType === periodType && stat.periodValue === periodValue);
|
|
702
|
+
if (existingStat) {
|
|
703
|
+
statsIdsToUpdate.push(existingStat.id);
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
statsToCreate.push({ packageId: pkg.id, periodType, periodValue, count: 1 });
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
await Promise.all([
|
|
710
|
+
...(statsToCreate.length > 0 ? [statsModel.bulkCreate(statsToCreate, { transaction })] : []),
|
|
711
|
+
...(statsIdsToUpdate.length > 0
|
|
712
|
+
? [statsModel.increment("count", { by: 1, where: { id: { [sequelize.Op.in]: statsIdsToUpdate } }, transaction })]
|
|
713
|
+
: []),
|
|
714
|
+
]);
|
|
637
715
|
}
|
|
638
716
|
async addPackageDownloadCount(packageName, version, transaction) {
|
|
639
717
|
const pkg = await this.ensurePackageExists(packageName, version, transaction);
|
|
640
|
-
return this.
|
|
718
|
+
return this.addStatsForAllPeriod(pkg, DownloadStats, transaction);
|
|
641
719
|
}
|
|
642
720
|
async addPackageManifestViewCount(packageName, version, transaction) {
|
|
643
721
|
const pkg = await this.ensurePackageExists(packageName, version, transaction);
|
|
644
|
-
return this.
|
|
722
|
+
return this.addStatsForAllPeriod(pkg, ManifestViewStats, transaction);
|
|
645
723
|
}
|
|
646
724
|
async addTotalDownloadCount(transaction) {
|
|
647
725
|
const universePkg = await this.ensureUniversePackageExists(transaction);
|
|
648
|
-
return this.
|
|
726
|
+
return this.addStatsForAllPeriod(universePkg, DownloadStats, transaction);
|
|
649
727
|
}
|
|
650
728
|
async addTotalManifestViewCount(transaction) {
|
|
651
729
|
const universePkg = await this.ensureUniversePackageExists(transaction);
|
|
652
|
-
return this.
|
|
730
|
+
return this.addStatsForAllPeriod(universePkg, ManifestViewStats, transaction);
|
|
653
731
|
}
|
|
654
732
|
async ensurePackageExists(packageName, version, transaction) {
|
|
655
733
|
const [pkg] = await Package.findOrCreate({ where: { name: packageName, version }, transaction });
|
|
@@ -659,6 +737,9 @@ class Database {
|
|
|
659
737
|
return pkg;
|
|
660
738
|
}
|
|
661
739
|
async ensureUniversePackageExists(transaction) {
|
|
740
|
+
if (this.universePackage) {
|
|
741
|
+
return this.universePackage;
|
|
742
|
+
}
|
|
662
743
|
const universePkg = await Package.findOne({
|
|
663
744
|
where: { name: UNIVERSE_PACKAGE_NAME, version: UNIVERSE_PACKAGE_VERSION },
|
|
664
745
|
transaction,
|
|
@@ -666,60 +747,9 @@ class Database {
|
|
|
666
747
|
if (!universePkg) {
|
|
667
748
|
throw new Error("Universe package not found");
|
|
668
749
|
}
|
|
750
|
+
this.universePackage = universePkg;
|
|
669
751
|
return universePkg;
|
|
670
752
|
}
|
|
671
|
-
init() {
|
|
672
|
-
Package.init({
|
|
673
|
-
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: sequelize.DataTypes.INTEGER },
|
|
674
|
-
name: { allowNull: false, type: sequelize.DataTypes.STRING(100) },
|
|
675
|
-
version: { allowNull: false, type: sequelize.DataTypes.STRING(50) },
|
|
676
|
-
displayName: {
|
|
677
|
-
type: sequelize.DataTypes.VIRTUAL,
|
|
678
|
-
get() {
|
|
679
|
-
return `${this.name}@${this.version}`;
|
|
680
|
-
},
|
|
681
|
-
set() {
|
|
682
|
-
throw new Error("Virtual property, cannot be set");
|
|
683
|
-
},
|
|
684
|
-
},
|
|
685
|
-
}, { sequelize: this.sequelize, tableName: "packages", underscored: true });
|
|
686
|
-
DownloadStats.init({
|
|
687
|
-
count: { allowNull: false, type: sequelize.DataTypes.BIGINT, defaultValue: 0 },
|
|
688
|
-
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: sequelize.DataTypes.INTEGER },
|
|
689
|
-
packageId: { allowNull: false, type: sequelize.DataTypes.INTEGER, references: { model: Package, key: "id" } },
|
|
690
|
-
periodType: { allowNull: false, type: sequelize.DataTypes.ENUM(...PERIOD_TYPES) },
|
|
691
|
-
periodValue: { allowNull: false, type: sequelize.DataTypes.STRING(20) },
|
|
692
|
-
}, {
|
|
693
|
-
sequelize: this.sequelize,
|
|
694
|
-
tableName: "download_stats",
|
|
695
|
-
underscored: true,
|
|
696
|
-
});
|
|
697
|
-
ManifestViewStats.init({
|
|
698
|
-
count: { allowNull: false, type: sequelize.DataTypes.BIGINT, defaultValue: 0 },
|
|
699
|
-
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: sequelize.DataTypes.INTEGER },
|
|
700
|
-
packageId: { allowNull: false, type: sequelize.DataTypes.INTEGER, references: { model: Package, key: "id" } },
|
|
701
|
-
periodType: { allowNull: false, type: sequelize.DataTypes.ENUM(...PERIOD_TYPES) },
|
|
702
|
-
periodValue: { allowNull: false, type: sequelize.DataTypes.STRING(20) },
|
|
703
|
-
}, { sequelize: this.sequelize, tableName: "manifest_view_stats", underscored: true });
|
|
704
|
-
Package.hasMany(DownloadStats, {
|
|
705
|
-
sourceKey: "id",
|
|
706
|
-
foreignKey: "packageId",
|
|
707
|
-
});
|
|
708
|
-
Package.hasMany(ManifestViewStats, {
|
|
709
|
-
sourceKey: "id",
|
|
710
|
-
foreignKey: "packageId",
|
|
711
|
-
});
|
|
712
|
-
DownloadStats.belongsTo(Package, {
|
|
713
|
-
targetKey: "id",
|
|
714
|
-
foreignKey: "packageId",
|
|
715
|
-
as: "package",
|
|
716
|
-
});
|
|
717
|
-
ManifestViewStats.belongsTo(Package, {
|
|
718
|
-
targetKey: "id",
|
|
719
|
-
foreignKey: "packageId",
|
|
720
|
-
as: "package",
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
753
|
}
|
|
724
754
|
|
|
725
755
|
class Plugin {
|
|
@@ -735,8 +765,22 @@ class Plugin {
|
|
|
735
765
|
this.options = options;
|
|
736
766
|
setLogger(options.logger);
|
|
737
767
|
this.parsedConfig = new ParsedPluginConfig(config, options.config);
|
|
738
|
-
|
|
739
|
-
void this.initDB();
|
|
768
|
+
const db = new Database(this.parsedConfig);
|
|
769
|
+
void this.initDB(db);
|
|
770
|
+
// close db on process termination
|
|
771
|
+
for (const signal of ["SIGINT", "SIGQUIT", "SIGTERM", "SIGHUP"]) {
|
|
772
|
+
process.once(signal, async () => {
|
|
773
|
+
try {
|
|
774
|
+
debug("Received signal %s, closing db...", signal);
|
|
775
|
+
await db.close();
|
|
776
|
+
debug("DB closed, good bye!");
|
|
777
|
+
}
|
|
778
|
+
catch (e) {
|
|
779
|
+
debug("Error closing db: %s", e.message);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
this.db = db;
|
|
740
784
|
}
|
|
741
785
|
getVersion() {
|
|
742
786
|
return this.version;
|
|
@@ -749,10 +793,10 @@ class Plugin {
|
|
|
749
793
|
middleware.register_middlewares(app);
|
|
750
794
|
}
|
|
751
795
|
}
|
|
752
|
-
async initDB() {
|
|
796
|
+
async initDB(db) {
|
|
753
797
|
try {
|
|
754
|
-
await
|
|
755
|
-
await
|
|
798
|
+
await db.authenticate();
|
|
799
|
+
await db.migrate();
|
|
756
800
|
}
|
|
757
801
|
catch (err) {
|
|
758
802
|
logger.error({ err }, "Failed to initialize database; @{err}");
|
package/lib/index.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import dayjs, { extend } from 'dayjs';
|
|
3
4
|
import advancedFormat from 'dayjs/plugin/advancedFormat';
|
|
4
5
|
import isoWeek from 'dayjs/plugin/isoWeek';
|
|
5
6
|
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
|
6
7
|
import weekYear from 'dayjs/plugin/weekYear';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import { tarballUtils } from '@verdaccio/core';
|
|
9
|
-
import { Model, DataTypes, Sequelize } from 'sequelize';
|
|
10
8
|
import buildDebug from 'debug';
|
|
9
|
+
import { tarballUtils } from '@verdaccio/core';
|
|
10
|
+
import { Model, DataTypes, Sequelize, Op } from 'sequelize';
|
|
11
11
|
import { Umzug, SequelizeStorage } from 'umzug';
|
|
12
12
|
|
|
13
13
|
var name = "verdaccio-stats";
|
|
14
|
-
var version = "0.3.
|
|
14
|
+
var version = "0.3.3";
|
|
15
15
|
|
|
16
16
|
const plugin = {
|
|
17
17
|
name,
|
|
@@ -98,6 +98,16 @@ function getPeriodValue(periodType, date, isoWeek) {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Get the period value for the current date.
|
|
103
|
+
*
|
|
104
|
+
* @param periodType {PeriodType} - The period type.
|
|
105
|
+
* @param isoWeek {boolean} - Whether to use ISO week format.
|
|
106
|
+
* @returns {PeriodValue} The period value for the current date.
|
|
107
|
+
*/
|
|
108
|
+
function getCurrentPeriodValue(periodType, isoWeek) {
|
|
109
|
+
return getPeriodValue(periodType, new Date(), isoWeek);
|
|
110
|
+
}
|
|
101
111
|
/**
|
|
102
112
|
* Check if the status code is a success status code.
|
|
103
113
|
*
|
|
@@ -206,7 +216,19 @@ const statsConfig = z
|
|
|
206
216
|
});
|
|
207
217
|
class ParsedPluginConfig {
|
|
208
218
|
verdaccioConfig;
|
|
219
|
+
config;
|
|
209
220
|
favicon = "/-/static/favicon.ico";
|
|
221
|
+
constructor(config, verdaccioConfig) {
|
|
222
|
+
this.verdaccioConfig = verdaccioConfig;
|
|
223
|
+
try {
|
|
224
|
+
this.config = statsConfig.parse(config);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
const fieldErrors = err.flatten().fieldErrors;
|
|
228
|
+
logger.error({ errors: fieldErrors }, "Invalid config for verdaccio stats plugin, @{errors}");
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
210
232
|
get configPath() {
|
|
211
233
|
return this.verdaccioConfig.configPath ?? this.verdaccioConfig.self_path;
|
|
212
234
|
}
|
|
@@ -247,18 +269,16 @@ class ParsedPluginConfig {
|
|
|
247
269
|
get title() {
|
|
248
270
|
return this.verdaccioConfig.web?.title ? `${this.verdaccioConfig.web.title} - Stats` : "Verdaccio Stats";
|
|
249
271
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const debug = buildDebug(`verdaccio:plugin:${pluginKey}`);
|
|
275
|
+
function getUmzugLogger() {
|
|
276
|
+
return {
|
|
277
|
+
debug: (msg) => debug("umzug:debug: %j", msg),
|
|
278
|
+
error: (msg) => debug("umzug:error: %j", msg),
|
|
279
|
+
info: (msg) => debug("umzug:info: %j", msg),
|
|
280
|
+
warn: (msg) => debug("umzug:warn: %j", msg),
|
|
281
|
+
};
|
|
262
282
|
}
|
|
263
283
|
|
|
264
284
|
class Hooks {
|
|
@@ -351,9 +371,11 @@ class Stats {
|
|
|
351
371
|
};
|
|
352
372
|
}
|
|
353
373
|
|
|
354
|
-
class
|
|
374
|
+
class StatsModel extends Model {
|
|
355
375
|
}
|
|
356
|
-
class
|
|
376
|
+
class DownloadStats extends StatsModel {
|
|
377
|
+
}
|
|
378
|
+
class ManifestViewStats extends StatsModel {
|
|
357
379
|
}
|
|
358
380
|
class Package extends Model {
|
|
359
381
|
}
|
|
@@ -400,7 +422,18 @@ class UI {
|
|
|
400
422
|
{
|
|
401
423
|
resource: Package,
|
|
402
424
|
options: {
|
|
403
|
-
actions: {
|
|
425
|
+
actions: {
|
|
426
|
+
...defaultActions,
|
|
427
|
+
search: {
|
|
428
|
+
before: (request) => {
|
|
429
|
+
if (request.params.action !== "search") {
|
|
430
|
+
return request;
|
|
431
|
+
}
|
|
432
|
+
request.query = { ...request.query, searchProperty: "name" };
|
|
433
|
+
return request;
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
404
437
|
titleProperty: "displayName",
|
|
405
438
|
listProperties: ["id", "name", "version", "createdAt"],
|
|
406
439
|
showProperties: ["id", "name", "version", "createdAt"],
|
|
@@ -408,38 +441,36 @@ class UI {
|
|
|
408
441
|
sort: { sortBy: "createdAt", direction: "desc" },
|
|
409
442
|
},
|
|
410
443
|
},
|
|
411
|
-
this.config.countDownloads &&
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
availableValues: PERIOD_TYPES.map((type) => ({ value: type, label: type })),
|
|
419
|
-
},
|
|
444
|
+
this.config.countDownloads && {
|
|
445
|
+
resource: DownloadStats,
|
|
446
|
+
options: {
|
|
447
|
+
actions: { ...defaultActions },
|
|
448
|
+
properties: {
|
|
449
|
+
periodType: {
|
|
450
|
+
availableValues: PERIOD_TYPES.map((type) => ({ value: type, label: type })),
|
|
420
451
|
},
|
|
421
|
-
listProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
422
|
-
showProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
423
|
-
filterProperties: ["packageId", "periodType", "periodValue", "createdAt", "updatedAt"],
|
|
424
|
-
sort: { sortBy: "updatedAt", direction: "desc" },
|
|
425
452
|
},
|
|
453
|
+
listProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
454
|
+
showProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
455
|
+
filterProperties: ["packageId", "periodType", "periodValue", "createdAt", "updatedAt"],
|
|
456
|
+
sort: { sortBy: "updatedAt", direction: "desc" },
|
|
426
457
|
},
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
},
|
|
458
|
+
},
|
|
459
|
+
this.config.countManifestViews && {
|
|
460
|
+
resource: ManifestViewStats,
|
|
461
|
+
options: {
|
|
462
|
+
actions: { ...defaultActions },
|
|
463
|
+
properties: {
|
|
464
|
+
periodType: {
|
|
465
|
+
availableValues: PERIOD_TYPES.map((type) => ({ value: type, label: type })),
|
|
436
466
|
},
|
|
437
|
-
listProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
438
|
-
showProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
439
|
-
filterProperties: ["packageId", "periodType", "periodValue", "createdAt", "updatedAt"],
|
|
440
|
-
sort: { sortBy: "updatedAt", direction: "desc" },
|
|
441
467
|
},
|
|
468
|
+
listProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
469
|
+
showProperties: ["id", "packageId", "periodType", "periodValue", "count", "createdAt", "updatedAt"],
|
|
470
|
+
filterProperties: ["packageId", "periodType", "periodValue", "createdAt", "updatedAt"],
|
|
471
|
+
sort: { sortBy: "updatedAt", direction: "desc" },
|
|
442
472
|
},
|
|
473
|
+
},
|
|
443
474
|
].filter(Boolean),
|
|
444
475
|
rootPath: rootPath,
|
|
445
476
|
branding: {
|
|
@@ -455,16 +486,6 @@ class UI {
|
|
|
455
486
|
}
|
|
456
487
|
}
|
|
457
488
|
|
|
458
|
-
const debug = buildDebug(`verdaccio:plugin:${pluginKey}`);
|
|
459
|
-
function getUmzugLogger() {
|
|
460
|
-
return {
|
|
461
|
-
debug: (msg) => debug("umzug:debug: %j", msg),
|
|
462
|
-
error: (msg) => debug("umzug:error: %j", msg),
|
|
463
|
-
info: (msg) => debug("umzug:info: %j", msg),
|
|
464
|
-
warn: (msg) => debug("umzug:warn: %j", msg),
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
|
|
468
489
|
const migrations = [
|
|
469
490
|
{
|
|
470
491
|
name: "000-initial",
|
|
@@ -534,15 +555,16 @@ class Database {
|
|
|
534
555
|
config;
|
|
535
556
|
sequelize;
|
|
536
557
|
umzug;
|
|
558
|
+
universePackage = null;
|
|
537
559
|
constructor(config) {
|
|
538
560
|
const sequelizeOptions = config.sequelizeOptions;
|
|
539
561
|
logger.debug({ dialect: sequelizeOptions.dialect }, "Creating @{dialect} database connection");
|
|
540
562
|
const sequelize = new Sequelize({
|
|
541
563
|
...sequelizeOptions,
|
|
542
|
-
logging: (sql) =>
|
|
564
|
+
logging: (sql) => debug(sql),
|
|
543
565
|
pool: {
|
|
544
|
-
max:
|
|
545
|
-
min:
|
|
566
|
+
max: 10,
|
|
567
|
+
min: 2,
|
|
546
568
|
acquire: 30_000,
|
|
547
569
|
idle: 10_000,
|
|
548
570
|
},
|
|
@@ -561,6 +583,60 @@ class Database {
|
|
|
561
583
|
this.umzug = umzug;
|
|
562
584
|
this.init();
|
|
563
585
|
}
|
|
586
|
+
init() {
|
|
587
|
+
Package.init({
|
|
588
|
+
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: DataTypes.INTEGER },
|
|
589
|
+
name: { allowNull: false, type: DataTypes.STRING(100) },
|
|
590
|
+
version: { allowNull: false, type: DataTypes.STRING(50) },
|
|
591
|
+
displayName: {
|
|
592
|
+
type: DataTypes.VIRTUAL(DataTypes.STRING, ["name", "version"]),
|
|
593
|
+
get() {
|
|
594
|
+
return `${this.getDataValue("name")}@${this.getDataValue("version")}`;
|
|
595
|
+
},
|
|
596
|
+
set() {
|
|
597
|
+
throw new Error("Virtual property, cannot be set");
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
}, { sequelize: this.sequelize, tableName: "packages", underscored: true });
|
|
601
|
+
DownloadStats.init({
|
|
602
|
+
count: { allowNull: false, type: DataTypes.BIGINT, defaultValue: 0 },
|
|
603
|
+
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: DataTypes.INTEGER },
|
|
604
|
+
packageId: { allowNull: false, type: DataTypes.INTEGER, references: { model: Package, key: "id" } },
|
|
605
|
+
periodType: { allowNull: false, type: DataTypes.ENUM(...PERIOD_TYPES), values: PERIOD_TYPES },
|
|
606
|
+
periodValue: { allowNull: false, type: DataTypes.STRING(20) },
|
|
607
|
+
}, {
|
|
608
|
+
sequelize: this.sequelize,
|
|
609
|
+
tableName: "download_stats",
|
|
610
|
+
underscored: true,
|
|
611
|
+
});
|
|
612
|
+
ManifestViewStats.init({
|
|
613
|
+
count: { allowNull: false, type: DataTypes.BIGINT, defaultValue: 0 },
|
|
614
|
+
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: DataTypes.INTEGER },
|
|
615
|
+
packageId: { allowNull: false, type: DataTypes.INTEGER, references: { model: Package, key: "id" } },
|
|
616
|
+
periodType: { allowNull: false, type: DataTypes.ENUM(...PERIOD_TYPES), values: PERIOD_TYPES },
|
|
617
|
+
periodValue: { allowNull: false, type: DataTypes.STRING(20) },
|
|
618
|
+
}, { sequelize: this.sequelize, tableName: "manifest_view_stats", underscored: true });
|
|
619
|
+
Package.hasMany(DownloadStats, {
|
|
620
|
+
sourceKey: "id",
|
|
621
|
+
foreignKey: "packageId",
|
|
622
|
+
as: "downloadStats",
|
|
623
|
+
});
|
|
624
|
+
Package.hasMany(ManifestViewStats, {
|
|
625
|
+
sourceKey: "id",
|
|
626
|
+
foreignKey: "packageId",
|
|
627
|
+
as: "manifestViewStats",
|
|
628
|
+
});
|
|
629
|
+
DownloadStats.belongsTo(Package, {
|
|
630
|
+
targetKey: "id",
|
|
631
|
+
foreignKey: "packageId",
|
|
632
|
+
as: "package",
|
|
633
|
+
});
|
|
634
|
+
ManifestViewStats.belongsTo(Package, {
|
|
635
|
+
targetKey: "id",
|
|
636
|
+
foreignKey: "packageId",
|
|
637
|
+
as: "package",
|
|
638
|
+
});
|
|
639
|
+
}
|
|
564
640
|
async addDownloadCount(packageName, version) {
|
|
565
641
|
const t = await this.sequelize.transaction();
|
|
566
642
|
try {
|
|
@@ -603,49 +679,51 @@ class Database {
|
|
|
603
679
|
rollback() {
|
|
604
680
|
return this.umzug.down();
|
|
605
681
|
}
|
|
606
|
-
async
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
packageId: pkg.id,
|
|
611
|
-
periodType,
|
|
612
|
-
periodValue: getPeriodValue(periodType, undefined, this.config.isoWeek),
|
|
613
|
-
},
|
|
614
|
-
transaction,
|
|
615
|
-
});
|
|
616
|
-
return downloadStats;
|
|
617
|
-
}));
|
|
618
|
-
return Promise.all(downloadStatsList.map((downloadStats) => downloadStats.increment("count", { by: 1, transaction })));
|
|
619
|
-
}
|
|
620
|
-
async addManifestViewForAllPeriod(pkg, transaction) {
|
|
621
|
-
const manifestViewStatsList = await Promise.all(PERIOD_TYPES.map(async (periodType) => {
|
|
622
|
-
const [manifestViewStats] = await ManifestViewStats.findOrCreate({
|
|
623
|
-
where: {
|
|
624
|
-
packageId: pkg.id,
|
|
625
|
-
periodType,
|
|
626
|
-
periodValue: getPeriodValue(periodType, undefined, this.config.isoWeek),
|
|
627
|
-
},
|
|
628
|
-
transaction,
|
|
629
|
-
});
|
|
630
|
-
return manifestViewStats;
|
|
682
|
+
async addStatsForAllPeriod(pkg, statsModel, transaction) {
|
|
683
|
+
const periodValues = PERIOD_TYPES.map((periodType) => ({
|
|
684
|
+
periodType,
|
|
685
|
+
periodValue: getCurrentPeriodValue(periodType, this.config.isoWeek),
|
|
631
686
|
}));
|
|
632
|
-
|
|
687
|
+
const existingStats = await statsModel.findAll({
|
|
688
|
+
where: {
|
|
689
|
+
packageId: pkg.id,
|
|
690
|
+
[Op.or]: periodValues,
|
|
691
|
+
},
|
|
692
|
+
transaction,
|
|
693
|
+
});
|
|
694
|
+
const statsToCreate = [];
|
|
695
|
+
const statsIdsToUpdate = [];
|
|
696
|
+
for (const { periodType, periodValue } of periodValues) {
|
|
697
|
+
const existingStat = existingStats.find((stat) => stat.periodType === periodType && stat.periodValue === periodValue);
|
|
698
|
+
if (existingStat) {
|
|
699
|
+
statsIdsToUpdate.push(existingStat.id);
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
statsToCreate.push({ packageId: pkg.id, periodType, periodValue, count: 1 });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
await Promise.all([
|
|
706
|
+
...(statsToCreate.length > 0 ? [statsModel.bulkCreate(statsToCreate, { transaction })] : []),
|
|
707
|
+
...(statsIdsToUpdate.length > 0
|
|
708
|
+
? [statsModel.increment("count", { by: 1, where: { id: { [Op.in]: statsIdsToUpdate } }, transaction })]
|
|
709
|
+
: []),
|
|
710
|
+
]);
|
|
633
711
|
}
|
|
634
712
|
async addPackageDownloadCount(packageName, version, transaction) {
|
|
635
713
|
const pkg = await this.ensurePackageExists(packageName, version, transaction);
|
|
636
|
-
return this.
|
|
714
|
+
return this.addStatsForAllPeriod(pkg, DownloadStats, transaction);
|
|
637
715
|
}
|
|
638
716
|
async addPackageManifestViewCount(packageName, version, transaction) {
|
|
639
717
|
const pkg = await this.ensurePackageExists(packageName, version, transaction);
|
|
640
|
-
return this.
|
|
718
|
+
return this.addStatsForAllPeriod(pkg, ManifestViewStats, transaction);
|
|
641
719
|
}
|
|
642
720
|
async addTotalDownloadCount(transaction) {
|
|
643
721
|
const universePkg = await this.ensureUniversePackageExists(transaction);
|
|
644
|
-
return this.
|
|
722
|
+
return this.addStatsForAllPeriod(universePkg, DownloadStats, transaction);
|
|
645
723
|
}
|
|
646
724
|
async addTotalManifestViewCount(transaction) {
|
|
647
725
|
const universePkg = await this.ensureUniversePackageExists(transaction);
|
|
648
|
-
return this.
|
|
726
|
+
return this.addStatsForAllPeriod(universePkg, ManifestViewStats, transaction);
|
|
649
727
|
}
|
|
650
728
|
async ensurePackageExists(packageName, version, transaction) {
|
|
651
729
|
const [pkg] = await Package.findOrCreate({ where: { name: packageName, version }, transaction });
|
|
@@ -655,6 +733,9 @@ class Database {
|
|
|
655
733
|
return pkg;
|
|
656
734
|
}
|
|
657
735
|
async ensureUniversePackageExists(transaction) {
|
|
736
|
+
if (this.universePackage) {
|
|
737
|
+
return this.universePackage;
|
|
738
|
+
}
|
|
658
739
|
const universePkg = await Package.findOne({
|
|
659
740
|
where: { name: UNIVERSE_PACKAGE_NAME, version: UNIVERSE_PACKAGE_VERSION },
|
|
660
741
|
transaction,
|
|
@@ -662,60 +743,9 @@ class Database {
|
|
|
662
743
|
if (!universePkg) {
|
|
663
744
|
throw new Error("Universe package not found");
|
|
664
745
|
}
|
|
746
|
+
this.universePackage = universePkg;
|
|
665
747
|
return universePkg;
|
|
666
748
|
}
|
|
667
|
-
init() {
|
|
668
|
-
Package.init({
|
|
669
|
-
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: DataTypes.INTEGER },
|
|
670
|
-
name: { allowNull: false, type: DataTypes.STRING(100) },
|
|
671
|
-
version: { allowNull: false, type: DataTypes.STRING(50) },
|
|
672
|
-
displayName: {
|
|
673
|
-
type: DataTypes.VIRTUAL,
|
|
674
|
-
get() {
|
|
675
|
-
return `${this.name}@${this.version}`;
|
|
676
|
-
},
|
|
677
|
-
set() {
|
|
678
|
-
throw new Error("Virtual property, cannot be set");
|
|
679
|
-
},
|
|
680
|
-
},
|
|
681
|
-
}, { sequelize: this.sequelize, tableName: "packages", underscored: true });
|
|
682
|
-
DownloadStats.init({
|
|
683
|
-
count: { allowNull: false, type: DataTypes.BIGINT, defaultValue: 0 },
|
|
684
|
-
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: DataTypes.INTEGER },
|
|
685
|
-
packageId: { allowNull: false, type: DataTypes.INTEGER, references: { model: Package, key: "id" } },
|
|
686
|
-
periodType: { allowNull: false, type: DataTypes.ENUM(...PERIOD_TYPES) },
|
|
687
|
-
periodValue: { allowNull: false, type: DataTypes.STRING(20) },
|
|
688
|
-
}, {
|
|
689
|
-
sequelize: this.sequelize,
|
|
690
|
-
tableName: "download_stats",
|
|
691
|
-
underscored: true,
|
|
692
|
-
});
|
|
693
|
-
ManifestViewStats.init({
|
|
694
|
-
count: { allowNull: false, type: DataTypes.BIGINT, defaultValue: 0 },
|
|
695
|
-
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: DataTypes.INTEGER },
|
|
696
|
-
packageId: { allowNull: false, type: DataTypes.INTEGER, references: { model: Package, key: "id" } },
|
|
697
|
-
periodType: { allowNull: false, type: DataTypes.ENUM(...PERIOD_TYPES) },
|
|
698
|
-
periodValue: { allowNull: false, type: DataTypes.STRING(20) },
|
|
699
|
-
}, { sequelize: this.sequelize, tableName: "manifest_view_stats", underscored: true });
|
|
700
|
-
Package.hasMany(DownloadStats, {
|
|
701
|
-
sourceKey: "id",
|
|
702
|
-
foreignKey: "packageId",
|
|
703
|
-
});
|
|
704
|
-
Package.hasMany(ManifestViewStats, {
|
|
705
|
-
sourceKey: "id",
|
|
706
|
-
foreignKey: "packageId",
|
|
707
|
-
});
|
|
708
|
-
DownloadStats.belongsTo(Package, {
|
|
709
|
-
targetKey: "id",
|
|
710
|
-
foreignKey: "packageId",
|
|
711
|
-
as: "package",
|
|
712
|
-
});
|
|
713
|
-
ManifestViewStats.belongsTo(Package, {
|
|
714
|
-
targetKey: "id",
|
|
715
|
-
foreignKey: "packageId",
|
|
716
|
-
as: "package",
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
749
|
}
|
|
720
750
|
|
|
721
751
|
class Plugin {
|
|
@@ -731,8 +761,22 @@ class Plugin {
|
|
|
731
761
|
this.options = options;
|
|
732
762
|
setLogger(options.logger);
|
|
733
763
|
this.parsedConfig = new ParsedPluginConfig(config, options.config);
|
|
734
|
-
|
|
735
|
-
void this.initDB();
|
|
764
|
+
const db = new Database(this.parsedConfig);
|
|
765
|
+
void this.initDB(db);
|
|
766
|
+
// close db on process termination
|
|
767
|
+
for (const signal of ["SIGINT", "SIGQUIT", "SIGTERM", "SIGHUP"]) {
|
|
768
|
+
process.once(signal, async () => {
|
|
769
|
+
try {
|
|
770
|
+
debug("Received signal %s, closing db...", signal);
|
|
771
|
+
await db.close();
|
|
772
|
+
debug("DB closed, good bye!");
|
|
773
|
+
}
|
|
774
|
+
catch (e) {
|
|
775
|
+
debug("Error closing db: %s", e.message);
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
this.db = db;
|
|
736
780
|
}
|
|
737
781
|
getVersion() {
|
|
738
782
|
return this.version;
|
|
@@ -745,10 +789,10 @@ class Plugin {
|
|
|
745
789
|
middleware.register_middlewares(app);
|
|
746
790
|
}
|
|
747
791
|
}
|
|
748
|
-
async initDB() {
|
|
792
|
+
async initDB(db) {
|
|
749
793
|
try {
|
|
750
|
-
await
|
|
751
|
-
await
|
|
794
|
+
await db.authenticate();
|
|
795
|
+
await db.migrate();
|
|
752
796
|
}
|
|
753
797
|
catch (err) {
|
|
754
798
|
logger.error({ err }, "Failed to initialize database; @{err}");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Application, Handler } from "express";
|
|
2
2
|
import type { ConfigHolder } from "../config";
|
|
3
|
-
import type { PluginMiddleware } from "../types";
|
|
4
3
|
import { Database } from "../storage/db";
|
|
4
|
+
import type { PluginMiddleware } from "../types";
|
|
5
5
|
export declare class Hooks implements PluginMiddleware {
|
|
6
6
|
private config;
|
|
7
7
|
private db;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Application } from "express";
|
|
2
2
|
import type { ConfigHolder } from "../config";
|
|
3
|
-
import type { PluginMiddleware } from "../types";
|
|
4
3
|
import { Database } from "../storage/db";
|
|
4
|
+
import type { PluginMiddleware } from "../types";
|
|
5
5
|
/**
|
|
6
6
|
* NOTE: This middleware is not implemented yet.
|
|
7
7
|
*/
|
package/lib/migrations.d.ts
CHANGED
package/lib/models.d.ts
CHANGED
|
@@ -1,24 +1,50 @@
|
|
|
1
|
-
import { CreationOptional, ForeignKey, type InferAttributes, type InferCreationAttributes, Model, NonAttribute } from "sequelize";
|
|
1
|
+
import { Association, type CreationOptional, type ForeignKey, type HasManyAddAssociationMixin, type HasManyAddAssociationsMixin, type HasManyCountAssociationsMixin, type HasManyCreateAssociationMixin, type HasManyGetAssociationsMixin, type HasManyHasAssociationMixin, type HasManyHasAssociationsMixin, type HasManyRemoveAssociationMixin, type HasManyRemoveAssociationsMixin, type HasManySetAssociationsMixin, type InferAttributes, type InferCreationAttributes, Model, type NonAttribute } from "sequelize";
|
|
2
2
|
import type { PeriodType, PeriodValue } from "./types";
|
|
3
|
-
export declare class
|
|
4
|
-
count: number;
|
|
3
|
+
export declare class StatsModel<T extends Model = Model> extends Model<InferAttributes<T>, InferCreationAttributes<T>> {
|
|
5
4
|
id: CreationOptional<number>;
|
|
6
|
-
package?: NonAttribute<Package>;
|
|
7
|
-
packageId: ForeignKey<Package["id"]>;
|
|
8
|
-
periodType: PeriodType;
|
|
9
|
-
periodValue: PeriodValue;
|
|
10
|
-
}
|
|
11
|
-
export declare class ManifestViewStats extends Model<InferAttributes<ManifestViewStats>, InferCreationAttributes<ManifestViewStats>> {
|
|
12
5
|
count: number;
|
|
13
|
-
id: CreationOptional<number>;
|
|
14
|
-
package?: NonAttribute<Package>;
|
|
15
6
|
packageId: ForeignKey<Package["id"]>;
|
|
16
7
|
periodType: PeriodType;
|
|
17
8
|
periodValue: PeriodValue;
|
|
9
|
+
package?: NonAttribute<Package>;
|
|
18
10
|
}
|
|
19
|
-
export declare class
|
|
20
|
-
|
|
11
|
+
export declare class DownloadStats extends StatsModel<DownloadStats> {
|
|
12
|
+
}
|
|
13
|
+
export declare class ManifestViewStats extends StatsModel<ManifestViewStats> {
|
|
14
|
+
}
|
|
15
|
+
export declare class Package extends Model<InferAttributes<Package, {
|
|
16
|
+
omit: "downloadStats" | "manifestViewStats";
|
|
17
|
+
}>, InferCreationAttributes<Package, {
|
|
18
|
+
omit: "downloadStats" | "manifestViewStats";
|
|
19
|
+
}>> {
|
|
21
20
|
id: CreationOptional<number>;
|
|
22
21
|
name: string;
|
|
23
22
|
version: string;
|
|
23
|
+
readonly displayName: string;
|
|
24
|
+
downloadStats?: NonAttribute<DownloadStats[]>;
|
|
25
|
+
manifestViewStats?: NonAttribute<ManifestViewStats[]>;
|
|
26
|
+
getDownloadStats: HasManyGetAssociationsMixin<DownloadStats>;
|
|
27
|
+
countDownloadStats: HasManyCountAssociationsMixin;
|
|
28
|
+
hasDownloadStat: HasManyHasAssociationMixin<DownloadStats, number>;
|
|
29
|
+
hasDownloadStats: HasManyHasAssociationsMixin<DownloadStats, number>;
|
|
30
|
+
setDownloadStats: HasManySetAssociationsMixin<DownloadStats, number>;
|
|
31
|
+
addDownloadStat: HasManyAddAssociationMixin<DownloadStats, number>;
|
|
32
|
+
addDownloadStats: HasManyAddAssociationsMixin<DownloadStats, number>;
|
|
33
|
+
removeDownloadStat: HasManyRemoveAssociationMixin<DownloadStats, number>;
|
|
34
|
+
removeDownloadStats: HasManyRemoveAssociationsMixin<DownloadStats, number>;
|
|
35
|
+
createDownloadStat: HasManyCreateAssociationMixin<DownloadStats, "packageId">;
|
|
36
|
+
getManifestViewStats: HasManyGetAssociationsMixin<ManifestViewStats>;
|
|
37
|
+
countManifestViewStats: HasManyCountAssociationsMixin;
|
|
38
|
+
hasManifestViewStat: HasManyHasAssociationMixin<ManifestViewStats, number>;
|
|
39
|
+
hasManifestViewStats: HasManyHasAssociationsMixin<ManifestViewStats, number>;
|
|
40
|
+
setManifestViewStats: HasManySetAssociationsMixin<ManifestViewStats, number>;
|
|
41
|
+
addManifestViewStat: HasManyAddAssociationMixin<ManifestViewStats, number>;
|
|
42
|
+
addManifestViewStats: HasManyAddAssociationsMixin<ManifestViewStats, number>;
|
|
43
|
+
removeManifestViewStat: HasManyRemoveAssociationMixin<ManifestViewStats, number>;
|
|
44
|
+
removeManifestViewStats: HasManyRemoveAssociationsMixin<ManifestViewStats, number>;
|
|
45
|
+
createManifestViewStat: HasManyCreateAssociationMixin<ManifestViewStats, "packageId">;
|
|
46
|
+
static associations: {
|
|
47
|
+
downloadStats: Association<Package, DownloadStats>;
|
|
48
|
+
manifestViewStats: Association<Package, ManifestViewStats>;
|
|
49
|
+
};
|
|
24
50
|
}
|
package/lib/storage/db.d.ts
CHANGED
|
@@ -3,20 +3,20 @@ export declare class Database {
|
|
|
3
3
|
private config;
|
|
4
4
|
private sequelize;
|
|
5
5
|
private umzug;
|
|
6
|
+
private universePackage;
|
|
6
7
|
constructor(config: ConfigHolder);
|
|
8
|
+
private init;
|
|
7
9
|
addDownloadCount(packageName: string, version: string): Promise<void>;
|
|
8
10
|
addManifestViewCount(packageName: string, version?: string): Promise<void>;
|
|
9
11
|
authenticate(): Promise<void>;
|
|
10
12
|
close(): Promise<void>;
|
|
11
13
|
migrate(): Promise<import("umzug").MigrationMeta[]>;
|
|
12
14
|
rollback(): Promise<import("umzug").MigrationMeta[]>;
|
|
13
|
-
private
|
|
14
|
-
private addManifestViewForAllPeriod;
|
|
15
|
+
private addStatsForAllPeriod;
|
|
15
16
|
private addPackageDownloadCount;
|
|
16
17
|
private addPackageManifestViewCount;
|
|
17
18
|
private addTotalDownloadCount;
|
|
18
19
|
private addTotalManifestViewCount;
|
|
19
20
|
private ensurePackageExists;
|
|
20
21
|
private ensureUniversePackageExists;
|
|
21
|
-
private init;
|
|
22
22
|
}
|
package/lib/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ConfigType } from "dayjs";
|
|
2
2
|
import type { PeriodType, PeriodValue } from "./types";
|
|
3
3
|
/**
|
|
4
4
|
* Add a scope to the package name.
|
|
@@ -16,7 +16,15 @@ export declare function addScope(scope: string, packageName: string): string;
|
|
|
16
16
|
* @param isoWeek {boolean} - Whether to use ISO week format.
|
|
17
17
|
* @returns {PeriodValue} The period value.
|
|
18
18
|
*/
|
|
19
|
-
export declare function getPeriodValue(periodType: PeriodType, date?:
|
|
19
|
+
export declare function getPeriodValue(periodType: PeriodType, date?: ConfigType, isoWeek?: boolean): PeriodValue;
|
|
20
|
+
/**
|
|
21
|
+
* Get the period value for the current date.
|
|
22
|
+
*
|
|
23
|
+
* @param periodType {PeriodType} - The period type.
|
|
24
|
+
* @param isoWeek {boolean} - Whether to use ISO week format.
|
|
25
|
+
* @returns {PeriodValue} The period value for the current date.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getCurrentPeriodValue(periodType: PeriodType, isoWeek?: boolean): PeriodValue;
|
|
20
28
|
/**
|
|
21
29
|
* Check if the status code is a success status code.
|
|
22
30
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "verdaccio-stats",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "The stats plugin for Verdaccio",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"module": "./lib/index.mjs",
|
|
@@ -17,38 +17,38 @@
|
|
|
17
17
|
"author": "Xingwang Liao<kuoruan@gmail.com>",
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@eslint/js": "^9.
|
|
20
|
+
"@eslint/js": "^9.27.0",
|
|
21
21
|
"@rollup/plugin-commonjs": "^28.0.3",
|
|
22
22
|
"@rollup/plugin-json": "^6.1.0",
|
|
23
23
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
24
24
|
"@rollup/plugin-replace": "^6.0.2",
|
|
25
25
|
"@rollup/plugin-typescript": "^12.1.2",
|
|
26
26
|
"@types/debug": "^4.1.12",
|
|
27
|
-
"@types/express": "^4.17.
|
|
27
|
+
"@types/express": "^4.17.22",
|
|
28
28
|
"@verdaccio/types": "13.0.0-next-8.3",
|
|
29
29
|
"cross-env": "^7.0.3",
|
|
30
|
-
"eslint": "^9.
|
|
31
|
-
"eslint-config-prettier": "^10.1.
|
|
32
|
-
"eslint-import-resolver-next": "^0.
|
|
33
|
-
"eslint-plugin-import-x": "^4.
|
|
34
|
-
"eslint-plugin-
|
|
35
|
-
"eslint-plugin-
|
|
36
|
-
"eslint-plugin-unicorn": "^
|
|
30
|
+
"eslint": "^9.27.0",
|
|
31
|
+
"eslint-config-prettier": "^10.1.5",
|
|
32
|
+
"eslint-import-resolver-next": "^0.6.0",
|
|
33
|
+
"eslint-plugin-import-x": "^4.13.3",
|
|
34
|
+
"eslint-plugin-prettier": "^5.4.0",
|
|
35
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
36
|
+
"eslint-plugin-unicorn": "^59.0.1",
|
|
37
37
|
"express": "^4.21.2",
|
|
38
|
-
"globals": "^16.
|
|
39
|
-
"pg": "^8.
|
|
38
|
+
"globals": "^16.2.0",
|
|
39
|
+
"pg": "^8.16.0",
|
|
40
40
|
"pg-hstore": "^2.3.4",
|
|
41
41
|
"prettier": "^3.5.3",
|
|
42
42
|
"rimraf": "^6.0.1",
|
|
43
|
-
"rollup": "^4.
|
|
43
|
+
"rollup": "^4.41.1",
|
|
44
44
|
"rollup-plugin-node-externals": "^8.0.0",
|
|
45
45
|
"sqlite3": "^5.1.7",
|
|
46
46
|
"tslib": "^2.8.1",
|
|
47
|
-
"typescript": "^5.8.
|
|
48
|
-
"typescript-eslint": "^8.
|
|
49
|
-
"verdaccio": "^6.
|
|
47
|
+
"typescript": "^5.8.3",
|
|
48
|
+
"typescript-eslint": "^8.33.0",
|
|
49
|
+
"verdaccio": "^6.1.2",
|
|
50
50
|
"verdaccio-stats": "file:",
|
|
51
|
-
"vitest": "^3.
|
|
51
|
+
"vitest": "^3.1.4"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"verdaccio": "^5.0.0||^6.0.0"
|
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@adminjs/express": "^6.1.1",
|
|
61
61
|
"@adminjs/sequelize": "^4.1.1",
|
|
62
|
-
"@verdaccio/core": "8.0.0-next-8.
|
|
63
|
-
"adminjs": "^7.8.
|
|
62
|
+
"@verdaccio/core": "8.0.0-next-8.15",
|
|
63
|
+
"adminjs": "^7.8.16",
|
|
64
64
|
"dayjs": "^1.11.13",
|
|
65
|
-
"debug": "^4.4.
|
|
66
|
-
"sequelize": "^6.37.
|
|
65
|
+
"debug": "^4.4.1",
|
|
66
|
+
"sequelize": "^6.37.7",
|
|
67
67
|
"umzug": "^3.8.2",
|
|
68
|
-
"zod": "^3.
|
|
68
|
+
"zod": "^3.25.34"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"start": "cross-env DEBUG='verdaccio:*' node_modules/verdaccio/bin/verdaccio -c verdaccio/verdaccio.yml",
|