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 CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  This plugin adds detailed statistics functionality to your Verdaccio private npm registry.
4
4
 
5
+ [![npm](https://img.shields.io/npm/v/verdaccio-stats.svg)](https://www.npmjs.com/package/verdaccio-stats)
6
+ [![npm](https://img.shields.io/npm/dw/verdaccio-stats.svg)](https://www.npmjs.com/package/verdaccio-stats)
7
+ [![npm](https://img.shields.io/npm/dt/verdaccio-stats.svg)](https://www.npmjs.com/package/verdaccio-stats)
8
+ [![npm](https://img.shields.io/npm/l/verdaccio-stats.svg)](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
@@ -1,4 +1,4 @@
1
- import type { UmzugOptions } from "umzug";
2
1
  import buildDebug from "debug";
2
+ import type { UmzugOptions } from "umzug";
3
3
  export declare const debug: buildDebug.Debugger;
4
4
  export declare function getUmzugLogger(): UmzugOptions["logger"];
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 path = require('node:path');
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.1";
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
- config;
255
- constructor(config, verdaccioConfig) {
256
- this.verdaccioConfig = verdaccioConfig;
257
- try {
258
- this.config = statsConfig.parse(config);
259
- }
260
- catch (err) {
261
- const fieldErrors = err.flatten().fieldErrors;
262
- logger.error({ errors: fieldErrors }, "Invalid config for verdaccio stats plugin, @{errors}");
263
- process.exit(1);
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 DownloadStats extends sequelize.Model {
378
+ class StatsModel extends sequelize.Model {
359
379
  }
360
- class ManifestViewStats extends sequelize.Model {
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: { ...defaultActions },
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
- resource: DownloadStats,
418
- options: {
419
- actions: { ...defaultActions },
420
- properties: {
421
- periodType: {
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
- this.config.countManifestViews &&
432
- {
433
- resource: ManifestViewStats,
434
- options: {
435
- actions: { ...defaultActions },
436
- properties: {
437
- periodType: {
438
- availableValues: PERIOD_TYPES.map((type) => ({ value: type, label: type })),
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) => buildDebug(sql),
568
+ logging: (sql) => debug(sql),
547
569
  pool: {
548
- max: 5,
549
- min: 0,
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 addDownloadForAllPeriod(pkg, transaction) {
611
- const downloadStatsList = await Promise.all(PERIOD_TYPES.map(async (periodType) => {
612
- const [downloadStats] = await DownloadStats.findOrCreate({
613
- where: {
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
- return Promise.all(manifestViewStatsList.map((manifestViewStats) => manifestViewStats.increment("count", { by: 1, transaction })));
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.addDownloadForAllPeriod(pkg, transaction);
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.addManifestViewForAllPeriod(pkg, transaction);
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.addDownloadForAllPeriod(universePkg, transaction);
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.addManifestViewForAllPeriod(universePkg, transaction);
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
- this.db = new Database(this.parsedConfig);
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 this.db.authenticate();
755
- await this.db.migrate();
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.1";
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
- config;
251
- constructor(config, verdaccioConfig) {
252
- this.verdaccioConfig = verdaccioConfig;
253
- try {
254
- this.config = statsConfig.parse(config);
255
- }
256
- catch (err) {
257
- const fieldErrors = err.flatten().fieldErrors;
258
- logger.error({ errors: fieldErrors }, "Invalid config for verdaccio stats plugin, @{errors}");
259
- process.exit(1);
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 DownloadStats extends Model {
374
+ class StatsModel extends Model {
355
375
  }
356
- class ManifestViewStats extends Model {
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: { ...defaultActions },
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
- resource: DownloadStats,
414
- options: {
415
- actions: { ...defaultActions },
416
- properties: {
417
- periodType: {
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
- this.config.countManifestViews &&
428
- {
429
- resource: ManifestViewStats,
430
- options: {
431
- actions: { ...defaultActions },
432
- properties: {
433
- periodType: {
434
- availableValues: PERIOD_TYPES.map((type) => ({ value: type, label: type })),
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) => buildDebug(sql),
564
+ logging: (sql) => debug(sql),
543
565
  pool: {
544
- max: 5,
545
- min: 0,
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 addDownloadForAllPeriod(pkg, transaction) {
607
- const downloadStatsList = await Promise.all(PERIOD_TYPES.map(async (periodType) => {
608
- const [downloadStats] = await DownloadStats.findOrCreate({
609
- where: {
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
- return Promise.all(manifestViewStatsList.map((manifestViewStats) => manifestViewStats.increment("count", { by: 1, transaction })));
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.addDownloadForAllPeriod(pkg, transaction);
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.addManifestViewForAllPeriod(pkg, transaction);
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.addDownloadForAllPeriod(universePkg, transaction);
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.addManifestViewForAllPeriod(universePkg, transaction);
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
- this.db = new Database(this.parsedConfig);
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 this.db.authenticate();
751
- await this.db.migrate();
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
  */
@@ -1,3 +1,3 @@
1
- import type { RunnableMigration } from "umzug";
2
1
  import { type Sequelize } from "sequelize";
2
+ import type { RunnableMigration } from "umzug";
3
3
  export declare const migrations: RunnableMigration<Sequelize>[];
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 DownloadStats extends Model<InferAttributes<DownloadStats>, InferCreationAttributes<DownloadStats>> {
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 Package extends Model<InferAttributes<Package>, InferCreationAttributes<Package>> {
20
- readonly displayName: string;
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
  }
@@ -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 addDownloadForAllPeriod;
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 Dayjs } from "dayjs";
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?: Dayjs, isoWeek?: boolean): PeriodValue;
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.1",
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.22.0",
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.21",
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.22.0",
31
- "eslint-config-prettier": "^10.1.1",
32
- "eslint-import-resolver-next": "^0.4.2",
33
- "eslint-plugin-import-x": "^4.6.1",
34
- "eslint-plugin-perfectionist": "^4.10.1",
35
- "eslint-plugin-prettier": "^5.2.3",
36
- "eslint-plugin-unicorn": "^57.0.0",
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.0.0",
39
- "pg": "^8.14.0",
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.35.0",
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.2",
48
- "typescript-eslint": "^8.26.1",
49
- "verdaccio": "^6.0.5",
47
+ "typescript": "^5.8.3",
48
+ "typescript-eslint": "^8.33.0",
49
+ "verdaccio": "^6.1.2",
50
50
  "verdaccio-stats": "file:",
51
- "vitest": "^3.0.8"
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.12",
63
- "adminjs": "^7.8.15",
62
+ "@verdaccio/core": "8.0.0-next-8.15",
63
+ "adminjs": "^7.8.16",
64
64
  "dayjs": "^1.11.13",
65
- "debug": "^4.4.0",
66
- "sequelize": "^6.37.6",
65
+ "debug": "^4.4.1",
66
+ "sequelize": "^6.37.7",
67
67
  "umzug": "^3.8.2",
68
- "zod": "^3.24.2"
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",