Skip to content

Commit

Permalink
Track package downloads
Browse files Browse the repository at this point in the history
  • Loading branch information
codedmonkey committed Oct 8, 2024
1 parent c23f04e commit 94e4bea
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 15 deletions.
37 changes: 37 additions & 0 deletions migrations/Version20241008203020.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20241008203020 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add downloads';
}

public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE package_downloads (package_id INT NOT NULL, historical_data JSON NOT NULL, recent_data JSON NOT NULL, total INT NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, merged_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(package_id))');
$this->addSql('COMMENT ON COLUMN package_downloads.updated_at IS \'(DC2Type:date_immutable)\'');
$this->addSql('CREATE TABLE version_downloads (version_id INT NOT NULL, historical_data JSON NOT NULL, recent_data JSON NOT NULL, total INT NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, merged_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(version_id))');
$this->addSql('COMMENT ON COLUMN version_downloads.updated_at IS \'(DC2Type:date_immutable)\'');
$this->addSql('ALTER TABLE package_downloads ADD CONSTRAINT FK_E19D697EF44CABFF FOREIGN KEY (package_id) REFERENCES package (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE version_downloads ADD CONSTRAINT FK_45C4D6DB4BBC2705 FOREIGN KEY (version_id) REFERENCES version (id) NOT DEFERRABLE INITIALLY IMMEDIATE');

$this->addSql('INSERT INTO package_downloads (package_id, historical_data, recent_data, total) SELECT id, \'{}\', \'{}\', 0 FROM package');
$this->addSql('INSERT INTO version_downloads (version_id, historical_data, recent_data, total) SELECT id, \'{}\', \'{}\', 0 FROM version');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE package_downloads DROP CONSTRAINT FK_E19D697EF44CABFF');
$this->addSql('ALTER TABLE version_downloads DROP CONSTRAINT FK_45C4D6DB4BBC2705');
$this->addSql('DROP TABLE package_downloads');
$this->addSql('DROP TABLE version_downloads');
}
}
24 changes: 22 additions & 2 deletions src/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use CodedMonkey\Conductor\Doctrine\Entity\Package;
use CodedMonkey\Conductor\Doctrine\Repository\PackageRepository;
use CodedMonkey\Conductor\Doctrine\Repository\VersionRepository;
use CodedMonkey\Conductor\Message\TrackDownloads;
use CodedMonkey\Conductor\Message\UpdatePackage;
use CodedMonkey\Conductor\Package\PackageDistributionResolver;
use CodedMonkey\Conductor\Package\PackageMetadataResolver;
Expand All @@ -14,9 +15,12 @@
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\TransportNamesStamp;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\RouterInterface;
use function Symfony\Component\String\u;
Expand Down Expand Up @@ -131,9 +135,25 @@ public function packageDistribution(string $packageName, string $packageVersion,

#[Route('/downloads', name: 'api_track_downloads', methods: ['POST'])]
#[IsGrantedAccess]
public function trackDownloads(): Response
public function trackDownloads(Request $request): Response
{
return new Response();
$contents = json_decode($request->getContent(), true);
$invalidInputs = static function ($item) {
return !isset($item['name'], $item['version']);
};

if (!is_array($contents) || !isset($contents['downloads']) || !is_array($contents['downloads']) || array_filter($contents['downloads'], $invalidInputs)) {
return new JsonResponse(['status' => 'error', 'message' => 'Invalid request format, must be a json object containing a downloads key filled with an array of name/version objects'], 200);
}

$message = new TrackDownloads($contents['downloads'], new \DateTime());
$envelope = new Envelope($message, [
new TransportNamesStamp('async'),
]);

$this->messenger->dispatch($envelope);

return new JsonResponse(['status' => 'success'], Response::HTTP_CREATED);
}

private function findPackage(string $packageName): ?Package
Expand Down
130 changes: 130 additions & 0 deletions src/Doctrine/Entity/AbstractDownloads.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

namespace CodedMonkey\Conductor\Doctrine\Entity;

use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

abstract class AbstractDownloads
{
private ?array $data = null;

/**
* @var array<int|numeric-string, int> Data is keyed by date in form of YYYYMMDD and as such the keys are technically seen as ints by PHP
*/
#[ORM\Column(type: 'json')]
protected array $historicalData = [];

/**
* @var array<int|numeric-string, int> Data is keyed by date in form of YYYYMMDD and as such the keys are technically seen as ints by PHP
*/
#[ORM\Column(type: 'json')]
protected array $recentData = [];

#[ORM\Column(type: 'integer')]
protected int $total = 0;

#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
protected ?\DateTimeImmutable $updatedAt = null;

#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
protected ?\DateTimeImmutable $mergedAt = null;

/**
* @param array<int|numeric-string, int> $data
*/
public function setData(array $data): void
{
$this->historicalData = $data;
$this->recentData = [];

$this->mergedAt = new \DateTimeImmutable();

$this->data = null;
}

/**
* @param numeric-string $key
*/
public function setDataPoint(string $key, int $value): void
{
$this->recentData[$key] = $value;

$this->data = null;
}

/**
* @return array<int, int> Key is "YYYYMMDD" which means it always gets converted to an int by php
*/
public function getData(): array
{
if (null === $this->data) {
$this->data = $this->doMergeData();
}

return $this->data;
}

public function setTotal(int $total): void
{
$this->total = $total;
}

public function getTotal(): int
{
return $this->total;
}

public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}

public function setUpdatedAt(\DateTimeImmutable $updatedAt): void
{
$this->updatedAt = $updatedAt;
}

public function getMergedAt(): ?\DateTimeImmutable
{
return $this->mergedAt;
}

public function mergeData(): void
{
$data = $this->doMergeData();

$this->historicalData = $data;
$this->recentData = [];

$this->mergedAt = new \DateTimeImmutable();

$this->data = null;
}

protected function doMergeData(): array
{
$data = $this->historicalData;

foreach ($this->recentData as $dataKey => $dataPoint) {
$data[$dataKey] ??= 0;
$data[$dataKey] += $dataPoint;
}

return $data;
}

/**
* @param numeric-string $key
*/
public function increase(string $key): void
{
$this->recentData[$key] ??= 0;
$this->recentData[$key]++;

$this->total++;

$this->data = null;
$this->updatedAt = new \DateTimeImmutable();
}
}
35 changes: 22 additions & 13 deletions src/Doctrine/Entity/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ class Package
#[ORM\Column(nullable: true)]
private ?string $remoteId = null;

#[ORM\ManyToOne]
private ?Registry $mirrorRegistry = null;

#[ORM\OneToOne(mappedBy: 'package', cascade: ['persist'])]
private PackageDownloads $downloads;

/**
* @var Collection<int, Version>
*/
Expand All @@ -75,16 +81,14 @@ class Package
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?\DateTimeInterface $dumpedAt = null;

#[ORM\ManyToOne]
private ?Registry $mirrorRegistry = null;

/**
* @var array<string, Version>|null lookup table for versions
*/
private array|null $cachedVersions = null;

public function __construct()
{
$this->downloads = new PackageDownloads($this);
$this->versions = new ArrayCollection();
$this->createdAt = new \DateTime();
}
Expand Down Expand Up @@ -244,6 +248,21 @@ public function setRemoteId(?string $remoteId): void
$this->remoteId = $remoteId;
}

public function getMirrorRegistry(): ?Registry
{
return $this->mirrorRegistry;
}

public function setMirrorRegistry(?Registry $mirrorRegistry): void
{
$this->mirrorRegistry = $mirrorRegistry;
}

public function getDownloads(): PackageDownloads
{
return $this->downloads;
}

/**
* @return Collection<int, Version>
*/
Expand Down Expand Up @@ -303,16 +322,6 @@ public function setDumpedAt(?\DateTimeInterface $dumpedAt): void
$this->dumpedAt = $dumpedAt;
}

public function getMirrorRegistry(): ?Registry
{
return $this->mirrorRegistry;
}

public function setMirrorRegistry(?Registry $mirrorRegistry): void
{
$this->mirrorRegistry = $mirrorRegistry;
}

public static function sortVersions(Version $a, Version $b): int
{
$aVersion = $a->getNormalizedVersion();
Expand Down
23 changes: 23 additions & 0 deletions src/Doctrine/Entity/PackageDownloads.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace CodedMonkey\Conductor\Doctrine\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class PackageDownloads extends AbstractDownloads
{
#[ORM\Id]
#[ORM\OneToOne(inversedBy: 'downloads')]
private Package $package;

public function __construct(Package $package)
{
$this->package = $package;
}

public function getPackage(): Package
{
return $this->package;
}
}
9 changes: 9 additions & 0 deletions src/Doctrine/Entity/Version.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ class Version
#[ORM\ManyToOne(targetEntity: Package::class, inversedBy: 'versions')]
private ?Package $package;

#[ORM\OneToOne(mappedBy: 'version', cascade: ['persist'])]
private VersionDownloads $downloads;

#[ORM\Column(type: Types::DATETIME_MUTABLE)]
private \DateTimeInterface $createdAt;

Expand All @@ -130,6 +133,7 @@ public function __construct()
$this->replace = new ArrayCollection();
$this->suggest = new ArrayCollection();
$this->tags = new ArrayCollection();
$this->downloads = new VersionDownloads($this);
$this->createdAt = new \DateTime();
}

Expand Down Expand Up @@ -478,6 +482,11 @@ public function setPackage(Package $package): void
$this->package = $package;
}

public function getDownloads(): VersionDownloads
{
return $this->downloads;
}

public function getCreatedAt(): \DateTimeInterface
{
return $this->createdAt;
Expand Down
23 changes: 23 additions & 0 deletions src/Doctrine/Entity/VersionDownloads.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace CodedMonkey\Conductor\Doctrine\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class VersionDownloads extends AbstractDownloads
{
#[ORM\Id]
#[ORM\OneToOne(inversedBy: 'downloads')]
private Version $version;

public function __construct(Version $version)
{
$this->version = $version;
}

public function getVersion(): Version
{
return $this->version;
}
}
12 changes: 12 additions & 0 deletions src/Message/TrackDownloads.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace CodedMonkey\Conductor\Message;

readonly class TrackDownloads
{
public function __construct(
public array $downloads,
public \DateTimeInterface $downloadedAt,
) {
}
}
Loading

0 comments on commit 94e4bea

Please sign in to comment.