feat(monorepo): migrate to typescript monorepo

This commit is contained in:
2025-11-07 17:09:29 +02:00
committed by BernardNganduDev
parent 3e09956f05
commit 075a388ccb
745 changed files with 2341 additions and 5082 deletions
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Basango\Aggregator\Presentation\Console;
use Basango\Aggregator\Application\UseCase\Command\CreateSource;
use Basango\Aggregator\Domain\Model\ValueObject\Scoring\Bias;
use Basango\Aggregator\Domain\Model\ValueObject\Scoring\Credibility;
use Basango\Aggregator\Domain\Model\ValueObject\Scoring\Reliability;
use Basango\Aggregator\Domain\Model\ValueObject\Scoring\Transparency;
use Basango\SharedKernel\Application\Messaging\CommandBus;
use Basango\SharedKernel\Presentation\Console\AskArgumentFeature;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:create-source',
description: 'add a new data source'
)]
class CreateSourceConsole extends Command
{
use AskArgumentFeature;
private SymfonyStyle $io;
public function __construct(
private readonly CommandBus $commandBus
) {
parent::__construct();
}
#[\Override]
protected function configure(): void
{
$this->addArgument('source', InputArgument::REQUIRED, 'the website source to crawle');
$this->addArgument('displayName', InputArgument::OPTIONAL, 'the display name of the source');
$this->addArgument('description', InputArgument::OPTIONAL, 'the description of the source');
$this->addOption('bias', 'b', InputArgument::OPTIONAL, 'bias of the source', Bias::NEUTRAL->value);
$this->addOption('reliability', 'r', InputArgument::OPTIONAL, 'reliability of the source', Reliability::AVERAGE->value);
$this->addOption('transparency', 't', InputArgument::OPTIONAL, 'transparency of the source', Transparency::MEDIUM->value);
}
#[\Override]
protected function interact(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
$this->io->title('Create a new data source');
$this->askArgument($input, 'source');
$this->askArgument($input, 'displayName');
$this->askOption($input, 'bias');
$this->askOption($input, 'reliability');
$this->askOption($input, 'transparency');
}
#[\Override]
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
}
#[\Override]
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (! $this->io->confirm('Do you want to continue?', false)) {
$this->io->warning('Process aborted');
return Command::FAILURE;
}
/** @var string $source */
$source = $input->getArgument('source');
/** @var string|null $displayName */
$displayName = $input->getArgument('displayName');
/** @var string|null $description */
$description = $input->getArgument('description');
$credibility = new Credibility(
bias: Bias::from($input->getOption('bias')),
reliability: Reliability::from($input->getOption('reliability')),
transparency: Transparency::from($input->getOption('transparency')),
);
$this->commandBus->handle(new CreateSource($source, $credibility, $displayName, $description));
$this->io->success('Source add successfully');
return Command::SUCCESS;
}
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Basango\Aggregator\Presentation\Console;
use Basango\Aggregator\Application\UseCase\Command\DeleteArticles;
use Basango\SharedKernel\Application\Messaging\CommandBus;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:delete-articles',
description: 'remove all articles from the database by source',
)]
class DeleteArticlesConsole extends Command
{
private SymfonyStyle $io;
public function __construct(
private readonly CommandBus $commandBus
) {
parent::__construct();
}
#[\Override]
protected function configure(): void
{
$this->addArgument('source', InputArgument::REQUIRED, 'the website source to crawle');
$this->addOption('category', null, InputOption::VALUE_OPTIONAL, 'the category to crawle');
}
#[\Override]
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
}
#[\Override]
protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var string $source */
$source = $input->getArgument('source');
/** @var string|null $category */
$category = $input->getOption('category');
if (
$this->io->confirm('Delete all articles ?', false) &&
$this->io->confirm('Are you sure ?', false)
) {
$confirmation = $this->io->askQuestion(new Question('Specify the source to confirm : '));
if ($confirmation === $source) {
/** @var int $count */
$count = $this->commandBus->handle(new DeleteArticles($source, $category));
$this->io->success(sprintf('%d articles from %s removed', $count, $source));
} else {
$this->io->warning('Source does not match, aborting !');
}
}
return Command::SUCCESS;
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Basango\Aggregator\Presentation\Console;
use Basango\Aggregator\Application\UseCase\Command\ExportArticles;
use Basango\SharedKernel\Application\Messaging\CommandBus;
use Basango\SharedKernel\Domain\Model\ValueObject\DateRange;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:export-articles',
description: 'export crawled news website',
)]
final class ExportArticlesConsole extends Command
{
private SymfonyStyle $io;
public function __construct(
private readonly CommandBus $commandBus
) {
parent::__construct();
}
#[\Override]
protected function configure(): void
{
$this->addArgument('source', InputArgument::OPTIONAL, 'the website source to crawle');
$this->addOption('date', null, InputOption::VALUE_OPTIONAL, 'Date interval to crawle', null);
}
#[\Override]
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
}
#[\Override]
protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var string|null $source */
$source = $input->getArgument('source');
/** @var string|null $date */
$date = $input->getOption('date');
$confirmation = $this->io->confirm('This can take a while, would like to continue ?', false);
if ($confirmation) {
$this->commandBus->handle(new ExportArticles(
source: $source,
date: $date !== null ? DateRange::from($date) : null
));
}
$this->io->success('articles exported successfully');
return Command::SUCCESS;
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Basango\Aggregator\Presentation\Console;
use Basango\Aggregator\Application\ReadModel\SourceStatistics;
use Basango\Aggregator\Application\ReadModel\SourceStatisticsList;
use Basango\Aggregator\Application\UseCase\Query\GetSourceStatisticsList;
use Basango\SharedKernel\Application\Messaging\QueryBus;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Stopwatch\Stopwatch;
#[AsCommand(
name: 'app:stats',
description: 'show stats about the articles in the database',
)]
class GetSourceStatisticsListConsole extends Command
{
private SymfonyStyle $io;
public function __construct(
private readonly QueryBus $queryBus
) {
parent::__construct();
}
#[\Override]
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
}
#[\Override]
protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var SourceStatisticsList $stats */
$stats = $this->queryBus->handle(new GetSourceStatisticsList());
$stopWatch = new Stopwatch(true);
$stopWatch->start('app:stats');
$this->io->table(
['Source', 'Articles', 'Metadata', 'CrawledAt'],
array_map(
fn (SourceStatistics $source): array => [
$source->name,
number_format($source->articlesCount, decimal_separator: '.', thousands_separator: ','),
number_format($source->metadataAvailable, decimal_separator: '.', thousands_separator: ','),
$source->crawledAt?->format('Y-m-d H:i:s') ?? 'Never',
],
$stats->items
)
);
$this->io->text((string) $stopWatch->stop('app:stats'));
return Command::SUCCESS;
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Basango\Aggregator\Presentation\Web\Controller;
use Basango\Aggregator\Application\UseCase\Command\CreateArticle;
use Basango\Aggregator\Domain\Model\ValueObject\Link;
use Basango\Aggregator\Presentation\WriteModel\AddArticleModel;
use Basango\SharedKernel\Presentation\Web\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;
/**
* Class AddArticleController.
*
* @author bernard-ng <bernard@devscast.tech>
*/
final class AddArticleController extends AbstractController
{
public function __construct(
#[Autowire(env: 'BASANGO_CRAWLER_TOKEN')] private readonly string $token
) {
}
#[Route(
path: '/api/aggregator/articles',
name: 'aggregator_add_article',
requirements: [
'token' => Requirement::ASCII_SLUG,
],
methods: ['POST']
)]
public function __invoke(
#[MapQueryParameter] string $token,
#[MapRequestPayload] AddArticleModel $model
): JsonResponse {
if ($token !== $this->token) {
throw $this->createAccessDeniedException();
}
$this->handleCommand(new CreateArticle(
$model->title,
Link::from($model->link),
$model->categories,
$model->body,
$model->source,
$model->timestamp,
$model->metadata,
$model->tokenStatistics
));
return new JsonResponse(status: Response::HTTP_CREATED);
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Basango\Aggregator\Presentation\WriteModel;
use Basango\Aggregator\Domain\Model\ValueObject\OpenGraph;
use Basango\Aggregator\Domain\Model\ValueObject\TokenStatistics;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Class AddArticleModel.
*
* @author bernard-ng <bernard@devscast.tech>
*/
final class AddArticleModel
{
#[Assert\NotBlank]
public string $title;
#[Assert\NotBlank]
public string $link;
#[Assert\NotBlank]
public string $body;
#[Assert\NotBlank]
public string $source;
#[Assert\NotBlank]
public int $timestamp;
public array $categories = [];
public ?OpenGraph $metadata = null;
public ?TokenStatistics $tokenStatistics = null;
}